1 /******************************************************************************
2 
3     HTTP message header parser
4 
5     Link with
6 
7         -L-lglib-2.0
8 
9     .
10 
11     Copyright:
12         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
13         All rights reserved.
14 
15     License:
16         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
17         Alternatively, this file may be distributed under the terms of the Tango
18         3-Clause BSD License (see LICENSE_BSD.txt for details).
19 
20  ******************************************************************************/
21 
22 module ocean.net.http.message.HttpHeaderParser;
23 
24 
25 import ocean.meta.types.Qualifiers;
26 import ocean.core.Enforce;
27 import ocean.core.Verify;
28 import ocean.text.util.SplitIterator: ChrSplitIterator, ISplitIterator;
29 
30 import ocean.net.http.HttpException: HttpParseException;
31 
32 version (unittest) import ocean.core.Test;
33 
34 public alias long ssize_t;
35 
36 /******************************************************************************
37 
38     Searches the haystack[0 .. haystack_len], if haystack_len is >= 0, for the
39     first occurrence of the string needle. If haystack[0 .. haystack_len]
40     contains a NUL byte, the search will stop there.
41     If haystack_len is -1, haystack is assumed to be a NUL-terminated string and
42     needle is searched in the whole haystack string.
43 
44     This is a GLib function.
45 
46     @see http://developer.gnome.org/glib/stable/glib-String-Utility-Functions.html#g-strstr-len
47 
48     Params:
49         haystack     = haystack.ptr for haystack_len >= 0 or a pointer to a
50                        NUL-terminated string if haystack_len = -1
51         haystack_len = haystack.length or -1 if haystack is NUL-terminated
52         needle       = the string to search for (NUL-terminated)
53 
54     Returns:
55         a pointer to the found occurrence, or null if not found.
56 
57  ******************************************************************************/
58 
59 extern (C) private char* g_strstr_len(const(char)* haystack, ssize_t haystack_len, const(char)* needle);
60 
61 /******************************************************************************
62 
63     Interface for the header parser to get the parse results and set limits
64 
65  ******************************************************************************/
66 
67 interface IHttpHeaderParser
68 {
69     /**************************************************************************
70 
71         Header element
72 
73      **************************************************************************/
74 
75     struct HeaderElement
76     {
77         cstring key, val;
78     }
79 
80     /**************************************************************************
81 
82         Obtains a list of HeaderElement instances referring to the header lines
83         parsed so far. The key member of each element references the slice of
84         the corresponding header line before the first ':', the val member the
85         slice after the first ':'. Leading and tailing white space is trimmed
86         off both key and val.
87 
88         Returns:
89             list of HeaderElement instances referring to the header lines parsed
90             so far
91 
92      **************************************************************************/
93 
94     HeaderElement[] header_elements ( );
95 
96     /**************************************************************************
97 
98         Returns:
99             list of the the header lines parsed so far
100 
101      **************************************************************************/
102 
103     cstring[] header_lines ( );
104 
105     /**************************************************************************
106 
107         Returns:
108             limit for the number of HTTP message header lines
109 
110      **************************************************************************/
111 
112     size_t header_lines_limit ( );
113 
114     /**************************************************************************
115 
116         Sets the limit for the number of HTTP message header lines.
117 
118         Note: A buffer size is set to n elements so use a realistic value (not
119               uint.max for example).
120 
121         Params:
122             n = limit for the number of HTTP message header lines
123 
124         Returns:
125            limit for the number of HTTP message header lines
126 
127      **************************************************************************/
128 
129     size_t header_lines_limit ( size_t n );
130 
131     /**************************************************************************
132 
133         Returns:
134             limit for the number of HTTP message header lines
135 
136      **************************************************************************/
137 
138     size_t header_length_limit ( );
139 
140     /**************************************************************************
141 
142         Sets the HTTP message header size limit. This will reset the parse
143         state and clear the content.
144 
145         Note: A buffer size is set to n elements so use a realistic value (not
146               uint.max for example).
147 
148         Params:
149             n = HTTP message header size limit
150 
151         Returns:
152             HTTP message header size limit
153 
154      **************************************************************************/
155 
156     size_t header_length_limit ( size_t n );
157 }
158 
159 /******************************************************************************
160 
161     HttpHeaderParser class
162 
163  ******************************************************************************/
164 
165 class HttpHeaderParser : IHttpHeaderParser
166 {
167     /***************************************************************************
168 
169          Object pool index -- allows the construction of a pool of objects of
170          this type.
171 
172     ***************************************************************************/
173 
174     public size_t object_pool_index;
175 
176     /**************************************************************************
177 
178         Default values for header size limitation
179 
180      **************************************************************************/
181 
182     static immutable size_t DefaultSizeLimit  = 16 * 1024,
183                  DefaultLinesLimit = 64;
184 
185     /**************************************************************************
186 
187         Header lines split iterator
188 
189      **************************************************************************/
190 
191     private static class SplitHeaderLines : ISplitIterator
192     {
193         /**************************************************************************
194 
195             End-of-header-line token
196 
197          **************************************************************************/
198 
199         static immutable EndOfHeaderLine = "\r\n";
200 
201         /**************************************************************************
202 
203             Locates the first occurrence of the current delimiter string in str,
204             starting from str[start].
205 
206             Params:
207                  str     = string to scan for delimiter
208                  start   = search start index
209 
210             Returns:
211                  index of first occurrence of the current delimiter string in str or
212                  str.length if not found
213 
214          **************************************************************************/
215 
216         public override size_t locateDelim ( cstring str, size_t start = 0 )
217         {
218             verify(
219                 start < str.length,
220                 typeof (this).stringof ~
221                     ".locateDelim: start index out of range"
222             );
223             char* item = g_strstr_len(str.ptr + start, str.length - start, this.EndOfHeaderLine.ptr);
224 
225             return item? item - str.ptr : str.length;
226         }
227 
228         /**************************************************************************
229 
230             Skips the delimiter which str starts with.
231             Note that the result is correct only if str really starts with a
232             delimiter.
233 
234             Params:
235                 str = string starting with delimiter
236 
237             Returns:
238                 index of the first character after the starting delimiter in str
239 
240          **************************************************************************/
241 
242         protected override size_t skipDelim ( cstring str )
243         {
244             verify(str.length >= this.EndOfHeaderLine.length);
245             return this.EndOfHeaderLine.length;
246         }
247     }
248 
249     /**************************************************************************
250 
251          HTTP message header content buffer.
252          content.length determines the header length limit.
253 
254      **************************************************************************/
255 
256     private mstring content;
257 
258     /**************************************************************************
259 
260          Length of actual data in content.
261 
262      **************************************************************************/
263 
264     private size_t content_length;
265 
266     /**************************************************************************
267 
268         Position (index) in the content up to which the content has already been
269         parsed
270 
271      **************************************************************************/
272 
273     private size_t pos       = 0;
274 
275     /**************************************************************************
276 
277         false after reset() and before the start line is complete
278 
279      **************************************************************************/
280 
281     private bool have_start_line = false;
282 
283     /**************************************************************************
284 
285         Number of header lines parsed so far, excluding the start line
286 
287      **************************************************************************/
288 
289     private size_t n_header_lines = 0;
290 
291     /**************************************************************************
292 
293         Header lines, excluding the start line; elements slice this.content.
294 
295      **************************************************************************/
296 
297     private cstring[] header_lines_;
298 
299     /**************************************************************************
300 
301         Header elements
302 
303         "key" references the slice of the corresponding header line before the
304         first ':' and "val" after the first ':'. Leading and tailing white space
305         is trimmed off both key and val.
306 
307      **************************************************************************/
308 
309     private HeaderElement[] header_elements_;
310 
311     /**************************************************************************
312 
313         Reusable exception instance
314 
315      **************************************************************************/
316 
317     private HttpParseException exception;
318 
319     /**************************************************************************
320 
321         Indicates that the header is complete
322 
323      **************************************************************************/
324 
325     private bool finished = false;
326 
327     /**************************************************************************
328 
329         Counter consistency check
330 
331      **************************************************************************/
332 
333     invariant ( )
334     {
335         assert (this.pos <= this.content_length);
336         assert (this.header_elements_.length == this.header_lines_.length);
337         assert (this.n_header_lines <= this.header_lines_.length);
338 
339         assert (this.content_length <= this.content.length);
340     }
341 
342     /**************************************************************************
343 
344         Constructor
345 
346      **************************************************************************/
347 
348     public this ( )
349     {
350         this(this.DefaultSizeLimit, this.DefaultLinesLimit);
351     }
352 
353     /**************************************************************************
354 
355         Constructor
356 
357         Note: Each a buffer with size_limit and lines_limit elements is
358               allocated so use realistic values (not uint.max for example).
359 
360         Params:
361             size_limit  = HTTP message header size limit
362             lines_limit = limit for the number of HTTP message header lines
363 
364      **************************************************************************/
365 
366     public this ( size_t size_limit, size_t lines_limit )
367     {
368         this.exception        = new HttpParseException;
369         this.content          = new char[size_limit];
370         this.header_lines_    = new cstring[lines_limit];
371         this.header_elements_ = new HeaderElement[lines_limit];
372     }
373 
374     /**************************************************************************
375 
376         Start line tokens; slice the internal content buffer
377 
378      **************************************************************************/
379 
380     public cstring[3] start_line_tokens;
381 
382     /**************************************************************************
383 
384         Obtains a list of HeaderElement instances referring to the header lines
385         parsed so far. The key member of each element references the slice of
386         the corresponding header line before the first ':', the val member the
387         slice after the first ':'. Leading and tailing white space is trimmed
388         off both key and val.
389 
390         Returns:
391             list of HeaderElement instances referring to the header lines parsed
392             so far
393 
394      **************************************************************************/
395 
396     public override HeaderElement[] header_elements ( )
397     {
398         return this.header_elements_[0 .. this.n_header_lines];
399     }
400 
401     /**************************************************************************
402 
403         Returns:
404             list of the the header lines parsed so far
405 
406      **************************************************************************/
407 
408     public cstring[] header_lines ( )
409     {
410         return this.header_lines_[0 .. this.n_header_lines];
411     }
412 
413     /**************************************************************************
414 
415         Returns:
416             limit for the number of HTTP message header lines
417 
418      **************************************************************************/
419 
420     public override size_t header_lines_limit ( )
421     {
422         return this.header_lines_.length;
423     }
424 
425     /**************************************************************************
426 
427         Sets the limit for the number of HTTP message header lines.
428 
429         Note: A buffer size is set to n elements so use a realistic value (not
430               uint.max for example).
431 
432         Params:
433             n = limit for the number of HTTP message header lines
434 
435         Returns:
436            limit for the number of HTTP message header lines
437 
438      **************************************************************************/
439 
440     public override size_t header_lines_limit ( size_t n )
441     {
442         if (this.n_header_lines > n)
443         {
444             this.n_header_lines = n;
445         }
446 
447         return this.header_lines_.length = n;
448     }
449 
450     /**************************************************************************
451 
452         Returns:
453             HTTP message header size limit
454 
455      **************************************************************************/
456 
457     public override size_t header_length_limit ( )
458     {
459         return this.content.length;
460     }
461 
462     /**************************************************************************
463 
464         Sets the HTTP message header size limit. This will reset the parse
465         state and clear the content.
466 
467         Note: A buffer size is set to n elements so use a realistic value (not
468               uint.max for example).
469 
470         Params:
471             n = HTTP message header size limit
472 
473         Returns:
474             HTTP message header size limit
475 
476      **************************************************************************/
477 
478     public override size_t header_length_limit ( size_t n )
479     {
480         this.reset();
481 
482         return this.content.length = n;
483     }
484 
485     /**************************************************************************
486 
487         Resets the parse state and clears the content.
488 
489         Returns:
490             this instance
491 
492      **************************************************************************/
493 
494     typeof (this) reset ( )
495     {
496         this.start_line_tokens[] = null;
497 
498         this.pos            = 0;
499         this.content_length = 0;
500 
501         this.n_header_lines = 0;
502 
503         this.have_start_line = false;
504         this.finished        = false;
505 
506         return this;
507     }
508 
509     /**************************************************************************
510 
511         Parses content which is expected to be either the start of a HTTP
512         message or a HTTP message fragment that continues the content passed on
513         the last call to this method. Appends the slice of content which is part
514         of the HTTP message header (that is, everything before the end-of-header
515         token "\r\n\r\n" or content itself if it does not contain that token).
516         After the end of the message header has been reached, which is indicated
517         by a non-null return value, reset() must be called before calling this
518         method again.
519         Leading empty header lines are tolerated and ignored:
520 
521             "In the interest of robustness, servers SHOULD ignore any empty
522             line(s) received where a Request-Line is expected."
523 
524             @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.1
525 
526         Returns:
527             A slice of content after the end-of-header token (which may be an
528             empty string) or null if content does not contain the end-of-header
529             token.
530 
531         Throws:
532             HttpParseException
533                 - on parse error: if
534                     * the number of start line tokens is different from 3 or
535                     * a regular header_line does not contain a ':';
536                 - on limit excess: if
537                     * the header size in bytes exceeds the requested limit or
538                     * the number of header lines in exceeds the requested limit.
539 
540             Assert()s that this method is not called after the end of header had
541             been reacched.
542 
543      **************************************************************************/
544 
545     public cstring parse ( cstring content )
546     {
547         verify(!this.finished, "parse() called after finished");
548 
549         cstring msg_body_start = null;
550 
551         scope split_header = new SplitHeaderLines;
552 
553         split_header.include_remaining = false;
554 
555         cstring content_remaining = this.appendContent(content);
556 
557         foreach (header_line; split_header.reset(this.content[this.pos .. this.content_length]))
558         {
559             cstring remaining = split_header.remaining;
560 
561             if (header_line.length)
562             {
563                 if (this.have_start_line)
564                 {
565                     this.parseRegularHeaderLine(header_line);
566                 }
567                 else
568                 {
569                     this.parseStartLine(header_line);
570 
571                     this.have_start_line = true;
572                 }
573 
574                 this.pos = this.content_length - remaining.length;
575             }
576             else
577             {
578                 this.finished = this.have_start_line;                           // Ignore empty leading header lines
579 
580                 if (this.finished)
581                 {
582                     msg_body_start = remaining.length? remaining : content_remaining;
583                     break;
584                 }
585             }
586         }
587 
588         return msg_body_start;
589     }
590 
591     alias parse opCall;
592 
593     /**************************************************************************
594 
595         Appends content to this.content.
596 
597         Params:
598             content = content fragment to append
599 
600         Returns:
601             current content from the current parse position to the end of the
602             newly appended fragment
603 
604         Throws:
605             HttpParseException if the header size in bytes exceeds the requested
606             limit.
607 
608      **************************************************************************/
609 
610     private cstring appendContent ( cstring chunk )
611     out (remaining)
612     {
613         assert (remaining.length <= chunk.length);
614     }
615     do
616     {
617         verify(this.content_length <= this.content.length);
618 
619         size_t max_len  = this.content.length - this.content_length,
620                consumed = chunk.length;
621 
622         if (consumed > max_len)
623         {
624             /*
625              * If the chunk exceeds the header length limit, it may contain the
626              * start of the message body: Look for the end-of-header token in
627              * chunk[0 .. max_len]. If not found, the header is really too long.
628              */
629 
630             static immutable end_of_header = "\r\n\r\n";
631 
632             char* header_end = g_strstr_len(chunk.ptr, max_len, end_of_header.ptr);
633 
634             enforce(this.exception.set("request header too long: ")
635                     .append(this.start_line_tokens[1]),
636                     header_end !is null);
637 
638             consumed = (header_end - chunk.ptr) + end_of_header.length;
639 
640             verify(chunk[consumed - end_of_header.length .. consumed] == end_of_header);
641         }
642 
643         // Append chunk to this.content.
644 
645         size_t end = this.content_length + consumed;
646 
647         this.content[this.content_length .. end] = chunk[0 .. consumed];
648 
649         this.content_length = end;
650 
651         /*
652          * Return the tail of chunk that was not appended. This tail is empty
653          * unless chunk exceeded the header length limit and the end-of-header
654          * token was found in chunk.
655          */
656 
657         return chunk[consumed .. $];
658     }
659 
660     /**************************************************************************
661 
662         Parses header_line which is expected to be a regular HTTP message header
663         line (not the start line or the empty message header termination line).
664 
665         Params:
666             header_line = regular message header line
667 
668         Returns:
669             HeaderElement instance referring to the parsed line
670 
671         Throws:
672             HttpParseException
673                 - if the number of header lines exceeds the requested limit or
674                 - on parse error: if the header_line does not contain a ':'.
675 
676      **************************************************************************/
677 
678     private void parseRegularHeaderLine ( cstring header_line )
679     {
680 
681         enforce(this.exception.set("too many request header lines"),
682                 this.n_header_lines <= this.header_lines_.length);
683 
684         scope split_tokens = new ChrSplitIterator(':');
685 
686         split_tokens.collapse          = true;
687         split_tokens.include_remaining = false;
688 
689 
690         foreach (field_name; split_tokens.reset(header_line))
691         {
692             this.header_elements_[this.n_header_lines] = HeaderElement(ChrSplitIterator.trim(field_name),
693                                                                        ChrSplitIterator.trim(split_tokens.remaining));
694 
695             break;
696         }
697 
698         enforce(this.exception.set("invalid header line (no ':')"),
699                 split_tokens.n);
700 
701         this.header_lines_[this.n_header_lines++] = header_line;
702     }
703 
704     /**************************************************************************
705 
706         Parses start_line which is expected to be the HTTP message header start
707         line (not a regular header line or the empty message header termination
708         line).
709 
710         Params:
711             header_line = regular message header line
712 
713         Throws:
714             HttpParseException on parse error: if the number of start line
715             tokens is different from 3.
716 
717      **************************************************************************/
718 
719     private void parseStartLine ( cstring start_line )
720     {
721         scope split_tokens = new ChrSplitIterator(' ');
722 
723         split_tokens.collapse          = true;
724         split_tokens.include_remaining = true;
725 
726         uint i = 0;
727 
728         foreach (token; split_tokens.reset(start_line))
729         {
730             i = split_tokens.n;
731 
732             this.start_line_tokens[i - 1] = token;
733 
734             /*
735              * For http responses, the third token is the error description,
736              * which may contain spaces. eg,
737              * "HTTP/1.1 301 Moved Permanently"
738              *
739              * TODO: Replace this foreach with calls to split_tokens.next
740              */
741 
742             if (i >= this.start_line_tokens.length - 1)
743             {
744                 this.start_line_tokens[i] = split_tokens.remaining;
745                 ++i;
746                 break;
747             }
748         }
749 
750         enforce(this.exception.set("invalid start line (too few tokens)"),
751                 i == this.start_line_tokens.length);
752     }
753 }
754 
755 //version = OceanPerformanceTest;
756 
757 import core.stdc.time: time;
758 import core.sys.posix.stdlib: srand48, drand48;
759 
760 version (OceanPerformanceTest)
761 {
762     import ocean.io.Stdout;
763     import ocean.core.internal.gcInterface: gc_disable, gc_enable;
764 }
765 
766 unittest
767 {
768 
769     {
770         scope parser = new HttpHeaderParser;
771 
772         static immutable content1 = "POST / HTTP/1.1\r\n"      // 17
773                        ~ "Content-Length: 12\r\n"   // 37
774                        ~ "\r\n"                     // 39
775                        ~ "Hello World!";
776 
777 
778         parser.header_length_limit = 39;
779 
780         try
781         {
782             parser.parse(content1);
783         }
784         catch (HttpParseException e)
785         {
786             test(false);
787         }
788 
789         parser.reset();
790 
791         parser.header_length_limit = 38;
792 
793         try
794         {
795             parser.parse(content1);
796         }
797         catch (HttpParseException e) { }
798     }
799 
800     static immutable istring lorem_ipsum =
801         "Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod "
802       ~ "tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim "
803       ~ "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex "
804       ~ "ea commodi consequat. Quis aute iure reprehenderit in voluptate velit "
805       ~ "esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat "
806       ~ "cupiditat non proident, sunt in culpa qui officia deserunt mollit "
807       ~ "anim id est laborum. Duis autem vel eum iriure dolor in hendrerit in "
808       ~ "vulputate velit esse molestie consequat, vel illum dolore eu feugiat "
809       ~ "nulla facilisis at vero eros et accumsan et iusto odio dignissim qui "
810       ~ "blandit praesent luptatum zzril delenit augue duis dolore te feugait "
811       ~ "nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing "
812       ~ "elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna "
813       ~ "aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud "
814       ~ "exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea "
815       ~ "commodo consequat. Duis autem vel eum iriure dolor in hendrerit in "
816       ~ "vulputate velit esse molestie consequat, vel illum dolore eu feugiat "
817       ~ "nulla facilisis at vero eros et accumsan et iusto odio dignissim qui "
818       ~ "blandit praesent luptatum zzril delenit augue duis dolore te feugait "
819       ~ "nulla facilisi. Nam liber tempor cum soluta nobis eleifend option "
820       ~ "congue nihil imperdiet doming id quod mazim placerat facer possim "
821       ~ "assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed "
822       ~ "diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam "
823       ~ "erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci "
824       ~ "tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo "
825       ~ "consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate "
826       ~ "velit esse molestie consequat, vel illum dolore eu feugiat nulla "
827       ~ "facilisis. At vero eos et accusam et justo duo dolores et ea rebum. "
828       ~ "Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum "
829       ~ "dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing "
830       ~ "elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore "
831       ~ "magna aliquyam erat, sed diam voluptua. At vero eos et accusam et "
832       ~ "justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea "
833       ~ "takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor "
834       ~ "sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam "
835       ~ "dolore dolores duo eirmod eos erat, et nonumy sed tempor et et "
836       ~ "invidunt justo labore Stet clita ea et gubergren, kasd magna no "
837       ~ "rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum "
838       ~ "dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing "
839       ~ "elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore "
840       ~ "magna aliquyam erat. Consetetur sadipscing elitr, sed diam nonumy "
841       ~ "eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed "
842       ~ "diam voluptua. At vero eos et accusam et justo duo dolores et ea "
843       ~ "rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem "
844       ~ "ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur "
845       ~ "sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et "
846       ~ "dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam "
847       ~ "et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea "
848       ~ "takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor "
849       ~ "sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor "
850       ~ "invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. "
851       ~ "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita "
852       ~ "kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit "
853       ~ "amet.";
854 
855     static immutable istring content2 =
856       "POST /dir?query=Hello%20World!&abc=def&ghi HTTP/1.1\r\n"
857       ~ "Host: www.example.org:12345\r\n"
858       ~ "User-Agent: Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.17) Gecko/20110422 Ubuntu/9.10 (karmic) Firefox/3.6.17\r\n"
859       ~ "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
860       ~ "Accept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3\r\n"
861       ~ "Accept-Encoding: gzip,deflate\r\n"
862       ~ "Accept-Charset: UTF-8,*\r\n"
863       ~ "Keep-Alive: 115\r\n"
864       ~ "Connection: keep-alive\r\n"
865       ~ "Cache-Control: max-age=0\r\n"
866       ~ "\r\n" ~
867         lorem_ipsum;
868 
869     static immutable parts = 10;
870 
871     /*
872      * content will be split into parts parts where the length of each part is
873      * content.length / parts + d with d a random number in the range
874      * [-(content.length / parts) / 3, +(content.length / parts) / 3].
875      */
876 
877     static size_t random_chunk_length ( )
878     {
879         static immutable c = content2.length * (2.0f / (parts * 3));
880 
881         static assert (c >= 3, "too many parts");
882 
883         return cast (size_t) (c + cast (float) drand48() * c);
884     }
885 
886     srand48(time(null));
887 
888     scope parser = new HttpHeaderParser;
889 
890     version (OceanPerformanceTest)
891     {
892         static immutable n = 1000_000;
893     }
894     else
895     {
896         static immutable n = 10;
897     }
898 
899     version (OceanPerformanceTest)
900     {
901         gc_disable();
902 
903         scope (exit) gc_enable();
904     }
905 
906     for (uint i = 0; i < n; i++)
907     {
908         parser.reset();
909 
910         {
911             size_t next = random_chunk_length();
912 
913             cstring msg_body_start = parser.parse(content2[0 .. next]);
914 
915             while (msg_body_start is null)
916             {
917                 size_t pos = next;
918 
919                 next = pos + random_chunk_length();
920 
921                 if (next < content2.length)
922                 {
923                     msg_body_start = parser.parse(content2[pos .. next]);
924                 }
925                 else
926                 {
927                     msg_body_start = parser.parse(content2[pos .. content2.length]);
928 
929                     test (msg_body_start !is null);
930                     test (msg_body_start.length <= content2.length);
931                     test (msg_body_start == content2[content2.length - msg_body_start.length .. content2.length]);
932                 }
933             }
934         }
935 
936         test (parser.start_line_tokens[0]  == "POST");
937         test (parser.start_line_tokens[1]  == "/dir?query=Hello%20World!&abc=def&ghi");
938         test (parser.start_line_tokens[2]  == "HTTP/1.1");
939 
940         {
941             auto elements = parser.header_elements;
942 
943             with (elements[0]) test (key == "Host"            && val == "www.example.org:12345");
944             with (elements[1]) test (key == "User-Agent"      && val == "Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.17) Gecko/20110422 Ubuntu/9.10 (karmic) Firefox/3.6.17");
945             with (elements[2]) test (key == "Accept"          && val == "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
946             with (elements[3]) test (key == "Accept-Language" && val == "de-de,de;q=0.8,en-us;q=0.5,en;q=0.3");
947             with (elements[4]) test (key == "Accept-Encoding" && val == "gzip,deflate");
948             with (elements[5]) test (key == "Accept-Charset"  && val == "UTF-8,*");
949             with (elements[6]) test (key == "Keep-Alive"      && val == "115");
950             with (elements[7]) test (key == "Connection"      && val == "keep-alive");
951             with (elements[8]) test (key == "Cache-Control"   && val == "max-age=0");
952 
953             test (elements.length == 9);
954         }
955 
956         {
957             auto lines = parser.header_lines;
958 
959             test (lines[0] == "Host: www.example.org:12345");
960             test (lines[1] == "User-Agent: Mozilla/5.0 (X11; U; Linux i686; de; rv:1.9.2.17) Gecko/20110422 Ubuntu/9.10 (karmic) Firefox/3.6.17");
961             test (lines[2] == "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
962             test (lines[3] == "Accept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3");
963             test (lines[4] == "Accept-Encoding: gzip,deflate");
964             test (lines[5] == "Accept-Charset: UTF-8,*");
965             test (lines[6] == "Keep-Alive: 115");
966             test (lines[7] == "Connection: keep-alive");
967             test (lines[8] == "Cache-Control: max-age=0");
968 
969             test (lines.length == 9);
970         }
971 
972         version (OceanPerformanceTest)
973         {
974             uint j = i + 1;
975 
976             if (!(j % 10_000))
977             {
978                 Stderr(HttpHeaderParser.stringof)(' ')(j)("\n").flush();
979             }
980         }
981     }
982 }