1 /** 
2  * Internal parsing utilities for the server's HTTP request processing.
3  */
4 module handy_httpd.parse_utils;
5 
6 import std.typecons;
7 import std.conv;
8 import std.array;
9 import std.stdio;
10 import std.string;
11 import std.algorithm;
12 import std.uri;
13 import httparsed;
14 import handy_httpd.request : HttpRequest;
15 
16 /** 
17  * The header struct to use when parsing data.
18  */
19 public struct Header {
20     const(char)[] name;
21     const(char)[] value;
22 }
23 
24 /** 
25  * The message struct to use when parsing HTTP requests, using the httparsed library.
26  */
27 public struct Msg {
28     @safe pure nothrow @nogc:
29         void onMethod(const(char)[] method) { this.method = method; }
30 
31         void onUri(const(char)[] uri) { this.uri = uri; }
32 
33         int onVersion(const(char)[] ver) {
34             minorVer = parseHttpVersion(ver);
35             return minorVer >= 0 ? 0 : minorVer;
36         }
37 
38         void onHeader(const(char)[] name, const(char)[] value) {
39             this.m_headers[m_headersLength].name = name;
40             this.m_headers[m_headersLength++].value = value;
41         }
42 
43         void onStatus(int status) { this.status = status; }
44 
45         void onStatusMsg(const(char)[] statusMsg) { this.statusMsg = statusMsg; }
46 
47         void reset() {
48             this.m_headersLength = 0;
49         }
50 
51     public const(char)[] method;
52     public const(char)[] uri;
53     public int minorVer;
54     public int status;
55     public const(char)[] statusMsg;
56 
57     private Header[64] m_headers;
58     private size_t m_headersLength;
59 
60     Header[] headers() return { return m_headers[0..m_headersLength]; }
61 }
62 
63 /** 
64  * Parses an HTTP request from a string.
65  * Params:
66  *   s = The raw HTTP request string.
67  * Returns: An HttpRequest object which can be passed to a handler.
68  */
69 public HttpRequest parseRequest(MsgParser!Msg requestParser, string s) {
70     // requestParser.msg.m_headersLength = 0; // Reset the parser headers.
71     int result = requestParser.parseRequest(s);
72     if (result != s.length) {
73         throw new Exception("Error! parse result doesn't match length. " ~ result.to!string);
74     }
75     string[string] headers;
76     foreach (h; requestParser.headers) {
77         headers[h.name] = cast(string) h.value;
78     }
79     string rawUrl = decode(cast(string) requestParser.uri);
80     auto urlAndParams = parseUrlAndParams(rawUrl);
81 
82     return HttpRequest(
83         cast(string) requestParser.method,
84         urlAndParams[0],
85         requestParser.minorVer,
86         headers,
87         urlAndParams[1]
88     );
89 }
90 
91 /** 
92  * Parses a path and set of query parameters from a raw URL string.
93  * Params:
94  *   rawUrl = The raw url containing both path and query params.
95  * Returns: A tuple containing the path and parsed query params.
96  */
97 private Tuple!(string, string[string]) parseUrlAndParams(string rawUrl) {
98     Tuple!(string, string[string]) result;
99     auto p = rawUrl.indexOf('?');
100     if (p == -1) {
101         result[0] = rawUrl;
102         result[1] = null;
103     } else {
104         result[0] = rawUrl[0..p];
105         result[1] = parseQueryString(rawUrl[p..$]);
106     }
107     // Strip away a trailing slash if there is one. This makes path matching easier.
108     if (result[0][$ - 1] == '/') {
109         result[0] = result[0][0 .. $ - 1];
110     }
111     return result;
112 }
113 
114 /** 
115  * Parses a set of query parameters from a query string.
116  * Params:
117  *   queryString = The raw query string to parse, including the preceding '?' character.
118  * Returns: An associative array containing parsed params.
119  */
120 private string[string] parseQueryString(string queryString) {
121     string[string] params;
122     if (queryString.length > 1) {
123         string[] paramSections = queryString[1..$].split("&").filter!(s => s.length > 0).array;
124         foreach (paramSection; paramSections) {
125             string paramName;
126             string paramValue;
127             auto p = paramSection.indexOf('=');
128             if (p == -1 || p + 1 == paramSection.length) {
129                 paramName = paramSection;
130                 paramValue = "true";
131             } else {
132                 paramName = paramSection[0..p];
133                 paramValue = paramSection[p+1..$];
134             }
135             params[paramName] = paramValue;
136         }
137     }
138     return params;
139 }