1 module atomized;
2 
3 private static
4 {
5     immutable string CARRIAGE_RETURN  = "\r\n";
6     immutable string HTTP_VERSION_1   = "HTTP/1.0";
7     immutable string HTTP_VERSION_1_1 = "HTTP/1.1";
8 
9     enum MessageParserState: ubyte
10     {
11         NONE=0,
12 
13         PARSING_START_LINE,
14         START_LINE_REQUEST,
15         START_LINE_RESPONSE,
16         HEADER_KEY,
17         HEADER_VALUE,
18         PARSING_BODY
19     }
20 
21     immutable string[ushort] statusCodes;
22 
23     static this()
24     {
25         import std.exception: assumeUnique;
26 
27         string[ushort] codes = [
28             100: "Continue",
29             101: "Switching Protocol",
30             200: "OK",
31             201: "Created",
32             202: "Accepted",
33             203: "Non-Authoritative Information",
34             204: "No Content",
35             205: "Reset Content",
36             206: "Partial Content",
37             300: "Multiple Choice",
38             301: "Moved Permanently",
39             302: "Found",
40             303: "See Other",
41             304: "Not Modified",
42             307: "Temporary Redirect",
43             308: "Permanent Redirect",
44             400: "Bad Request",
45             401: "Unauthorized",
46             402: "Payment Required",
47             403: "Forbidden",
48             404: "Not Found",
49             405: "Method Not Allowed",
50             406: "Not Acceptable",
51             407: "Proxy Authentication Required",
52             408: "Request Timeout",
53             409: "Conflict",
54             410: "Gone",
55             411: "Length Required",
56             412: "Precondition Failed",
57             413: "Payload Too Large",
58             414: "URI Too Long",
59             415: "Unsupported Media Type",
60             416: "Requested Range Not Satisfiable",
61             417: "Expectation Failed",
62             418: "I'm a teapot",
63             421: "Misdirected Request",
64             425: "Too Early",
65             426: "Upgrade Required",
66             428: "Precondition Required",
67             429: "Too Many Requests",
68             431: "Request Header Fields Too Large",
69             451: "Unavailable for Legal Reasons",
70             500: "Internal Server Error",
71             501: "Not Implemented",
72             502: "Bad Gateway",
73             503: "Service Unavailable",
74             504: "Gateway Timeout",
75             505: "HTTP Version Not Supported",
76             506: "Variant Also Negotiates",
77             507: "Insufficient Storage",
78             510: "Not Extended",
79             511: "Network Authentication Required"
80         ];
81 
82         codes.rehash;
83 
84         statusCodes = codes.assumeUnique;
85     }
86 
87     /**
88      * Converts a string to a MessageMethod for use in an HTTPMessage object.
89      */
90     MessageMethod stringToMessageMethod(const string method)
91     {
92         with(MessageMethod)
93         switch(method)
94         {
95             case "GET":      return GET;
96             case "HEAD":     return HEAD;
97             case "POST":     return POST;
98             case "PUT":      return PUT;
99             case "DELETE":   return DELETE;
100             case "CONNECT":  return CONNECT;
101             case "TRACE":    return TRACE;
102             case "PATCH":    return PATCH;
103 
104             default:         return NONE;
105         }
106     }
107 
108     /**
109      * To be returned with a status code in a response is a status text describing the
110      * status code by text rather than by a code.
111      * 
112      * This method takes in one of those codes and tries to return a text for it.
113      */
114     string statusTextFromStatusCode(const ushort statusCode)
115     {
116         if(statusCode in statusCodes)
117         {
118             return statusCodes[statusCode];
119         }
120 
121         return "Undefined";
122     }
123 }
124 
125 static enum MessageMethod: ubyte
126 {
127     NONE=0,
128 
129     GET,
130     HEAD,
131     POST,
132     PUT,
133     DELETE,
134     CONNECT,
135     TRACE,
136     PATCH
137 }
138 
139 class HTTPMessage
140 {
141 private:
142     /**
143      * The HTTP method for this message.
144      * 
145      * Defaults to `NONE` denoting a response.
146      */
147     MessageMethod m_method;
148 
149     /**
150      * A status code for this message.
151      * 
152      * This is ignored if this is a request, as requests have no notion of statuses.
153      */
154     ushort m_statusCode;
155 
156     /**
157      * A status message to be associated with the status code for this message.
158      * 
159      * Keep blank to use an automatically generated status message.
160      */
161     string m_statusMessage;
162 
163     /**
164      * The path for the resource specified in the message. Only used for a request.
165      * 
166      * Defaults to blank.
167      */
168     string m_path;
169 
170     /**
171      * The version used for this HTTP message as a string.
172      * 
173      * Defaults to "HTTP/1.1"
174      */
175     string m_version = HTTP_VERSION_1_1;
176 
177     /**
178      * An associative array of headers.
179      */
180     string[string] m_headers;
181 
182     /**
183      * An array of unsigned 8-bit integers used to store message bodies.
184      */
185     ubyte[] m_body;
186 
187 public:
188     /**
189      * Set a header in the map to the value provided.
190      */
191     HTTPMessage setHeader(const string name, const string value)
192     {
193         m_headers[name] = value;
194 
195         return this;
196     }
197 
198     /**
199      * Set a number of headers based on a generic map of keys and values.
200      */
201     HTTPMessage setHeaders(const string[string] headers)
202     {
203         foreach(key, value; headers)
204         {
205             m_headers[key] = value;
206         }
207 
208         return this;
209     }
210 
211     /**
212      * Get the string value of a single header from the message.
213      */
214     pragma(inline)
215     string getHeader(const string name)
216     {
217         return m_headers[name];
218     }
219 
220     /**
221      * Set the associated message method for this message.
222      * 
223      * Use `NONE` to switch this into a response.
224      */
225     HTTPMessage setMethod(const MessageMethod method)
226     {
227         m_method = method;
228 
229         return this;
230     }
231 
232     /**
233      * Grab the current method for this message.
234      * 
235      * Returns `NONE` if this is a response.
236      */
237     pragma(inline)
238     MessageMethod getMethod() const
239     {
240         return m_method;
241     }
242 
243     /**
244      * Set the path of this message, which will be used if it is a request.
245      */
246     HTTPMessage setPath(const string path)
247     {
248         m_path = path;
249 
250         return this;
251     }
252 
253     /**
254      * Grab the current associated path of this message.
255      */
256     pragma(inline)
257     string getPath()
258     {
259         return m_path;
260     }
261 
262     /**
263      * Set the version of this HTTP message to the string specified.
264      */
265     HTTPMessage setVersion(const string version_)
266     {
267         m_version = version_;
268 
269         return this;
270     }
271 
272     /**
273      * Get the current HTTP version for this message.
274      */
275     pragma(inline)
276     string getVersion()
277     {
278         return m_version;
279     }
280 
281     /**
282      * Set the status code of this HTTP message.
283      */
284     HTTPMessage setStatusCode(ushort code)
285     {
286         m_statusCode = code;
287         
288         return this;
289     }
290 
291     /**
292      * Get the status code for this message.
293      */
294     pragma(inline)
295     ushort getStatusCode()
296     {
297         return m_statusCode;
298     }
299 
300     /**
301      * Set the status message of this HTTP message.
302      */
303     HTTPMessage setStatusMessage(const string message)
304     {
305         m_statusMessage = message;
306 
307         return this;
308     }
309 
310     /**
311      * Get the current status message for this message.
312      * 
313      * Returns an autogenerated status if one isn't specified.
314      */
315     pragma(inline)
316     string getStatusMessage()
317     {
318         if (m_statusMessage.length == 0)
319         {
320             return statusTextFromStatusCode(m_statusCode);
321         }
322         else
323         {
324             return m_statusMessage;
325         }
326     }
327 
328     /**
329      * Takes the headers added to the message along with
330      * the body and outputs it to a `std::string` for use
331      * in client/server HTTP messages.
332      */
333     override string toString()
334     {
335         import std.conv: to;
336 
337         string output;
338 
339         // begin by forming the start line of the message
340         if (m_method == MessageMethod.NONE)
341         {
342             output ~= m_version ~ " " ~ m_statusCode.to!string ~ " ";
343             
344             if (m_statusMessage.length == 0)
345             {
346                 output ~= statusTextFromStatusCode(m_statusCode);
347             }
348             else
349             {
350                 output ~= m_statusMessage;
351             }
352         }
353         else
354         {
355             output ~= m_method.to!string ~ " ";
356             output ~= m_path ~ " ";
357             output ~= m_version;
358         }
359 
360         // output the status lines line break to move on
361         output ~= CARRIAGE_RETURN;
362 
363         // output headers to the message string
364         foreach(key, value; m_headers)
365             output ~= key ~ ": " ~ value ~ CARRIAGE_RETURN;
366 
367         // automatically output the content length based on
368         // the size of the body member if body isn't empty
369         if (m_body.length > 0)
370             output ~= "Content-Length: " ~ m_body.length.to!string ~ CARRIAGE_RETURN;
371 
372         // seperate headers and body with an extra carriage return
373         output ~= CARRIAGE_RETURN;
374         output ~= cast(string) m_body;
375 
376         return output;
377     }
378 
379     /**
380      * Set the body of this message to an unsigned 8-bit binary value.
381      */
382     HTTPMessage setMessageBody(const ubyte[] body_)
383     {
384         m_body = cast(ubyte[]) body_;
385 
386         return this;
387     }
388 
389     /**
390      * Set the body of this message to a string value.
391      */
392     HTTPMessage setMessageBody(const string body_)
393     {
394         return setMessageBody(cast(ubyte[]) body_);
395     }
396 
397     /**
398      * Get the body array for this message.
399      */
400     pragma(inline)
401     ubyte[] getMessageBody()
402     {
403         return m_body;
404     }
405 
406     /**
407      * Return the size of the binary body array.
408      */
409     pragma(inline)
410     size_t contentLength()
411     {
412         return m_body.length;
413     }
414 
415 }
416 
417 class HTTPMessageParser
418 {
419 public:
420     /**
421      * Parse a std::string to a HTTP message.
422      * 
423      * Pass in a pointer to an HTTPMessage which is then written to for headers
424      * and other message data.
425      * 
426      * note: this must be a complete HTTP message
427      */
428     void parse(ref HTTPMessage httpMessage, const string buffer)
429     {
430         parse(httpMessage, cast(ubyte[]) buffer);
431     }
432 
433     /**
434      * Parse an array of characters to an HTTP message.
435      * 
436      * Pass in a pointer to an HTTPMessage which is written to for headers and
437      * other message data.
438      * 
439      * note: must be a complete HTTP message.
440      */
441     void parse(ref HTTPMessage httpMessage, const ubyte[] buffer)
442     {
443         // begin by parsing the start line without knowing if it is a
444         // request or a response by setting as undetermined
445         MessageParserState state = MessageParserState.PARSING_START_LINE;
446     
447         // a temporary string instance used for storing characters of a
448         // current line in the message being parsed
449         string temp;
450 
451         // whether to skip the next character (for a carriage return)
452         bool skipNext = false;
453 
454         // the current key for a header
455         string headerKey;
456 
457         // whether or not a message body is present
458         bool hasMessageBody = false;
459 
460         // the index at which the message body begins
461         size_t bodyStartIndex = 0;
462 
463         for (size_t index = 0; index < buffer.length; index++)
464         {
465             ubyte character = buffer[index];
466 
467             // skip this character as it was marked
468             if (skipNext)
469             {
470                 skipNext = false;
471 
472                 continue;
473             }
474 
475             // if we are parsing the body, then we only need to grab an index and break
476             // out of this loop as we want to merely insert the data from this array
477             // into the body array
478             if (state == MessageParserState.PARSING_BODY)
479             {
480                 hasMessageBody = true;
481 
482                 bodyStartIndex = index;
483 
484                 break;
485             }
486 
487             // if we are parsing the start line but neither a response or request
488             if (state == MessageParserState.PARSING_START_LINE)
489             {
490                 // if we hit a space, we have to check if the start line begins
491                 // with the HTTP version or the method verb
492                 if (character == ' ')
493                 {
494                     // this message has a leading version string, thus it is
495                     // a response and not a request
496                     if (temp == HTTP_VERSION_1 || temp == HTTP_VERSION_1_1)
497                     {
498                         httpMessage.setMethod(MessageMethod.NONE);
499 
500                         state = MessageParserState.START_LINE_RESPONSE;
501 
502                         temp = "";
503 
504                         continue;
505                     }
506                     // this must be a request, so grab the MessageMethod type
507                     // for the request, set it, and move on
508                     else
509                     {
510                         httpMessage.setMethod(stringToMessageMethod(temp));
511 
512                         state = MessageParserState.START_LINE_REQUEST;
513                     
514                         temp = "";
515 
516                         continue;
517                     }
518                 }
519             }
520             // do actions for when the start line is a request
521             else if (state == MessageParserState.START_LINE_REQUEST)
522             {
523                 // once a space is hit, add the path to the message
524                 if (character == ' ')
525                 {
526                     httpMessage.setPath(temp);
527 
528                     temp = "";
529 
530                     continue;
531                 }
532                 // when the beginning of a carriage return is hit, add the version string
533                 // to the message and then skip the following new line character, setting
534                 // the state of the parser to be parsing headers
535                 else if (character == '\r')
536                 {
537                     httpMessage.setVersion(temp);
538 
539                     temp = "";
540 
541                     state = MessageParserState.HEADER_KEY;
542 
543                     skipNext = true;
544 
545                     continue;
546                 }
547             }
548             // do actions for when the start line is a response
549             else if (state == MessageParserState.START_LINE_RESPONSE)
550             {
551                 import std.conv: to;
552 
553                 // if we are at a space, then we have hit the status code for the response
554                 if (character == ' ')
555                 {
556                     httpMessage.setStatusCode(temp.to!ushort);
557 
558                     temp = "";
559 
560                     continue;
561                 }
562                 // if we are at a carriage return start, then set the status message for
563                 // the response, this can be blank in which it will use a generated status
564                 //
565                 // this will also set the state of the parser to move on to headers
566                 else if (character == '\r')
567                 {
568                     httpMessage.setStatusMessage(temp);
569 
570                     temp = "";
571 
572                     state = MessageParserState.HEADER_KEY;
573 
574                     skipNext = true;
575 
576                     continue;
577                 }
578             }
579             // if we are parsing header keys and hit a colon, then the key for the header has
580             // been fully parsed and should be added to the temporary key holder
581             else if (state == MessageParserState.HEADER_KEY && character == ':')
582             {
583                 headerKey = temp;
584 
585                 temp = "";
586 
587                 state = MessageParserState.HEADER_VALUE;
588 
589                 // HTTP defines that the next character in a header should be a space
590                 // so skip that for parsing the value of the header
591                 skipNext = true;
592 
593                 continue;
594             }
595             // if we are parsing header values and hit the beginning of a carriage return then
596             // it is time to add the header to the message with the key and value, and move the
597             // state back to parsing keys
598             else if (state == MessageParserState.HEADER_VALUE && character == '\r')
599             {
600                 httpMessage.setHeader(headerKey, temp);
601 
602                 headerKey = "";
603                 temp      = "";
604 
605                 state = MessageParserState.HEADER_KEY;
606 
607                 // skip the next character as it will just be a newline
608                 skipNext = true;
609 
610                 continue;
611             }
612             // if we are parsing header keys and we hit a carriage return, then we should assume
613             // that the headers have ended, and that we are now parsing a message body.
614             else if (state == MessageParserState.HEADER_KEY && character == '\r')
615             {
616                 temp = "";
617 
618                 state = MessageParserState.PARSING_BODY;
619 
620                 // skip the next character as it'll be a newline
621                 skipNext = true;
622 
623                 continue;
624             }
625 
626             temp ~= character;
627         }
628 
629         // add the body to the message if it is present
630         if (hasMessageBody)
631         {
632             httpMessage.setMessageBody(
633                 httpMessage.getMessageBody ~ buffer
634             );
635         }
636     }
637 }