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 }