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 // TODO: Before release we should remove this import 13 import std.stdio : writeln; 14 15 /* TODO: We could move these all to `package.d` */ 16 17 /* Static is redundant as module is always static , gshared needed */ 18 /* Apparebky works without gshared, that is kinda sus ngl */ 19 package __gshared Logger logger; 20 /** 21 * source/birchwood/messages.d(10,8): Error: variable `birchwood.messages.logger` is a thread-local class and cannot have a static initializer. Use `static this()` to initialize instead. 22 * 23 * It is complaining that it wopuld static init per thread, static this() for module is required but that would 24 * do a module init per thread, so __gshared static this() is needed, we want one global init - a single logger 25 * variable and also class init 26 */ 27 28 __gshared static this() 29 { 30 logger = new DefaultLogger(); 31 } 32 33 /** 34 * Encoding/decoding primitives 35 */ 36 37 /** 38 * Encodes the provided message into a CRLF 39 * terminated byte array 40 * 41 * Params: 42 * messageIn = the message to encode 43 * Returns: the encoded message 44 */ 45 public ubyte[] encodeMessage(string messageIn) 46 { 47 ubyte[] messageOut = cast(ubyte[])messageIn; 48 messageOut~=[cast(ubyte)13, cast(ubyte)10]; 49 return messageOut; 50 } 51 52 public static string decodeMessage(ubyte[] messageIn) 53 { 54 /* TODO: We could do a chekc to ESNURE it is well encoded */ 55 56 return cast(string)messageIn[0..messageIn.length-2]; 57 // return null; 58 } 59 60 /** 61 * Checks if the provided message is valid (i.e.) 62 * does not contain any CR or LF characters in it 63 * 64 * Params: 65 * message = the message to test 66 * Returns: <code>true</code> if the message is valid, 67 * <code>false</code> false otherwise 68 */ 69 //TODO: Should we add an emptiness check here 70 public static bool isValidText(string message) 71 { 72 foreach(char character; message) 73 { 74 if(character == 13 || character == 10) 75 { 76 return false; 77 } 78 } 79 80 return true; 81 } 82 83 /** 84 * Message types 85 */ 86 public final class Message 87 { 88 /* Message contents */ 89 private string from; 90 private string command; 91 private string params; 92 93 /* The numeric reply (as per Section 6 of RFC 1459) */ 94 private bool isNumericResponse = false; 95 private ReplyType replyType = ReplyType.BIRCHWOOD_UNKNOWN_RESP_CODE; 96 private bool isError = false; 97 98 /** 99 * Constructs a new Message 100 * 101 * Params: 102 * from = the from parameter 103 * command = the command 104 * params = any optional parameters to the command 105 */ 106 this(string from, string command, string params = "") 107 { 108 this.from = from; 109 this.command = command; 110 this.params = params; 111 112 /* Check if this is a command reply */ 113 if(isNumeric(command)) 114 { 115 isNumericResponse = true; 116 117 //FIXME: SOmething is tripping it u, elts' see 118 try 119 { 120 /* Grab the code */ 121 replyType = to!(ReplyType)(to!(ulong)(command)); 122 // TODO: Add validity check on range of values here, if bad throw exception 123 // TODO: Add check for "6.3 Reserved numerics" or handling of SOME sorts atleast 124 125 /* Error codes are in range of [401, 502] */ 126 if(replyType >= 401 && replyType <= 502) 127 { 128 // TODO: Call error handler 129 isError = true; 130 } 131 /* Command replies are in range of [259, 395] */ 132 else if(replyType >= 259 && replyType <= 395) 133 { 134 // TODO: Call command-reply handler 135 isError = false; 136 } 137 } 138 catch(ConvException e) 139 { 140 logger.log("<<< Unsupported response code (Error below) >>>"); 141 logger.log(e); 142 } 143 } 144 145 /* Parse the parameters into key-value pairs (if any) and trailing text (if any) */ 146 parameterParse(); 147 } 148 149 /* TODO: Implement encoder function */ 150 public string encode() 151 { 152 string fullLine = from~" "~command~" "~params; 153 return fullLine; 154 } 155 156 public static Message parseReceivedMessage(string message) 157 { 158 /* TODO: testing */ 159 160 /* From */ 161 string from; 162 163 /* Command */ 164 string command; 165 166 /* Params */ 167 string params; 168 169 170 171 /* Check if there is a PREFIX (according to RFC 1459) */ 172 if(message[0] == ':') 173 { 174 /* prefix ends after first space (we fetch servername, host/user) */ 175 //TODO: make sure not -1 176 long firstSpace = indexOf(message, ' '); 177 178 /* TODO: double check the condition */ 179 if(firstSpace > 0) 180 { 181 from = message[1..firstSpace]; 182 183 // logger.log("from: "~from); 184 185 /* TODO: Find next space (what follows `from` is `' ' { ' ' }`) */ 186 ulong i = firstSpace; 187 for(; i < message.length; i++) 188 { 189 if(message[i] != ' ') 190 { 191 break; 192 } 193 } 194 195 // writeln("Yo"); 196 197 string rem = message[i..message.length]; 198 // writeln("Rem: "~rem); 199 long idx = indexOf(rem, " "); //TOOD: -1 check 200 201 /* Extract the command */ 202 command = rem[0..idx]; 203 // logger.log("command: "~command); 204 205 /* Params are everything till the end */ 206 i = idx; 207 for(; i < rem.length; i++) 208 { 209 if(rem[i] != ' ') 210 { 211 break; 212 } 213 } 214 params = rem[i..rem.length]; 215 // logger.log("params: "~params); 216 } 217 else 218 { 219 //TODO: handle 220 logger.log("Malformed message start after :"); 221 assert(false); 222 } 223 224 225 } 226 /* In this case it is only `<command> <params>` */ 227 else 228 { 229 230 long firstSpace = indexOf(message, " "); //TODO: Not find check 231 232 command = message[0..firstSpace]; 233 234 ulong pos = firstSpace; 235 for(; pos < message.length; pos++) 236 { 237 if(message[pos] != ' ') 238 { 239 break; 240 } 241 } 242 243 params = message[pos..message.length]; 244 245 } 246 247 return new Message(from, command, params); 248 } 249 250 public override string toString() 251 { 252 return "(from: "~from~", command: "~command~", message: `"~params~"`)"; 253 } 254 255 /** 256 * Returns the sender of the message 257 * 258 * Returns: The `from` field 259 */ 260 public string getFrom() 261 { 262 return from; 263 } 264 265 /** 266 * Returns the command name 267 * 268 * Returns: The command itself 269 */ 270 public string getCommand() 271 { 272 return command; 273 } 274 275 /** 276 * Returns the optional paremeters (if any) 277 * 278 * Returns: The parameters 279 */ 280 public string getParams() 281 { 282 return params; 283 } 284 285 /** 286 * Retrieves the trailing text in the paramaters 287 * (if any) 288 * 289 * Returns: the trailing text 290 */ 291 public string getTrailing() 292 { 293 return ppTrailing; 294 } 295 296 /** 297 * Returns the parameters excluding the trailing text 298 * which are seperated by spaces but only those 299 * which are key-value pairs 300 * 301 * Returns: the key-value pair parameters 302 */ 303 public string[string] getKVPairs() 304 { 305 return ppKVPairs; 306 } 307 308 /** 309 * Returns the parameters excluding the trailing text 310 * which are seperated by spaces 311 * 312 * Returns: the parameters 313 */ 314 public string[] getPairs() 315 { 316 return ppPairs; 317 } 318 319 private string ppTrailing; 320 private string[string] ppKVPairs; 321 private string[] ppPairs; 322 323 324 version(unittest) 325 { 326 import std.stdio; 327 } 328 329 unittest 330 { 331 string testInput = "A:=1 A=2 :Hello this is text"; 332 writeln("Input: ", testInput); 333 334 bool hasTrailer; 335 string[] splitted = splitting(testInput, hasTrailer); 336 writeln("Input (split): ", splitted); 337 338 339 340 assert(cmp(splitted[0], "A:=1") == 0); 341 assert(cmp(splitted[1], "A=2") == 0); 342 343 /* Trailer test */ 344 assert(hasTrailer); 345 assert(cmp(splitted[2], "Hello this is text") == 0); 346 } 347 348 unittest 349 { 350 string testInput = ":Hello this is text"; 351 bool hasTrailer; 352 string[] splitted = splitting(testInput, hasTrailer); 353 354 /* Trailer test */ 355 assert(hasTrailer); 356 assert(cmp(splitted[0], "Hello this is text") == 0); 357 } 358 359 /** 360 * Imagine: `A:=1 A=2 :Hello` 361 * 362 * Params: 363 * input = 364 * Returns: 365 */ 366 private static string[] splitting(string input, ref bool hasTrailer) 367 { 368 string[] splits; 369 370 bool trailingMode; 371 string buildUp; 372 for(ulong idx = 0; idx < input.length; idx++) 373 { 374 /* Get current character */ 375 char curCHar = input[idx]; 376 377 378 if(trailingMode) 379 { 380 buildUp ~= curCHar; 381 continue; 382 } 383 384 if(buildUp.length == 0) 385 { 386 if(curCHar == ':') 387 { 388 trailingMode = true; 389 continue; 390 } 391 } 392 393 394 if(curCHar == ' ') 395 { 396 /* Flush */ 397 splits ~= buildUp; 398 buildUp = ""; 399 } 400 else 401 { 402 buildUp ~= curCHar; 403 } 404 } 405 406 if(buildUp.length) 407 { 408 splits ~= buildUp; 409 } 410 411 hasTrailer = trailingMode; 412 413 return splits; 414 } 415 416 /** 417 * NOTE: This needs more work with trailing support 418 * we must make sure we only look for lastInex of `:` 419 * where it is first cyaracter after space but NOT within 420 * an active parameter 421 */ 422 private void parameterParse() 423 { 424 /* Only parse if there are params */ 425 if(params.length) 426 { 427 /* Trailing text */ 428 string trailing; 429 430 /* Split the `<params>` */ 431 bool hasTrailer; 432 string[] paramsSplit = splitting(params, hasTrailer); 433 434 // logger.debug_("ParamsSPlit direct:", paramsSplit); 435 436 437 438 /* Extract the trailer as the last item in the array (if it exists) */ 439 if(hasTrailer) 440 { 441 trailing = paramsSplit[paramsSplit.length-1]; 442 443 /* Remove it from the parameters */ 444 paramsSplit = paramsSplit[0..$-1]; 445 446 // logger.debug_("GOt railer ", trailing); 447 } 448 449 ppPairs = paramsSplit; 450 451 452 /* Generate the key-value pairs */ 453 foreach(string pair; paramsSplit) 454 { 455 /* Only do this if we have an `=` in the current pair */ 456 if(indexOf(pair, "=") > -1) 457 { 458 string key = split(pair, "=")[0]; 459 string value = split(pair, "=")[1]; 460 ppKVPairs[key] = value; 461 } 462 } 463 464 /* Save the trailing */ 465 ppTrailing = trailing; 466 467 // logger.debug_("ppTrailing: ", ppTrailing); 468 } 469 } 470 471 /** 472 * Returns whether or not this message was 473 * a numeric response 474 * 475 * Returns: `true` if numeric response 476 * `false` otherwise 477 */ 478 public bool isResponseMessage() 479 { 480 return isNumericResponse; 481 } 482 483 /** 484 * Returns whether or not this message is 485 * an error kind-of numeric response 486 * 487 * Returns: `true` if numeric response 488 * is an error, `false` otherwise 489 */ 490 public bool isResponseError() 491 { 492 return isError; 493 } 494 495 /** 496 * Returns the type of reply (if this message 497 * was a numeric response) 498 * 499 * Returns: the ReplyType 500 */ 501 public ReplyType getReplyType() 502 { 503 return replyType; 504 } 505 }