1 /*
2  * Archttp - A highly performant web framework written in D.
3  *
4  * Copyright (C) 2021-2022 Kerisy.com
5  *
6  * Website: https://www.kerisy.com
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module archttp.HttpMessageParserTest;
13 
14 import archttp.HttpMessageParser;
15 
16 ///
17 @("example")
18 unittest
19 {
20     auto reqHandler = new HttpRequestParserHandler;
21     auto reqParser = new HttpMessageParser(reqHandler);
22 
23     auto resHandler = new HttpRequestParserHandler;
24     auto resParser = new HttpMessageParser(resHandler);
25 
26     // parse request
27     string data = "GET /foo HTTP/1.1\r\nHost: 127.0.0.1:8090\r\n\r\n";
28     // returns parsed message header length when parsed sucessfully, -ParserError on error
29     int res = reqParser.parseRequest(data);
30     assert(res == data.length);
31     assert(reqHandler.method == "GET");
32     assert(reqHandler.uri == "/foo");
33     assert(reqHandler.minorVer == 1); // HTTP/1.1
34     assert(reqHandler.headers.length == 1);
35     assert(reqHandler.headers[0].name == "Host");
36     assert(reqHandler.headers[0].value == "127.0.0.1:8090");
37 
38     // parse response
39     data = "HTTP/1.0 200 OK\r\n";
40     uint lastPos; // store last parsed position for next run
41     res = resParser.parseResponse(data, lastPos);
42     assert(res == -ParserError.partial); // no complete message header yet
43     data = "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 3\r\n\r\nfoo";
44     res = resParser.parseResponse(data, lastPos); // starts parsing from previous position
45     assert(res == data.length - 3); // whole message header parsed, body left to be handled based on actual header values
46     assert(resHandler.minorVer == 0); // HTTP/1.0
47     assert(resHandler.status == 200);
48     assert(resHandler.statusMsg == "OK");
49     assert(resHandler.headers.length == 2);
50     assert(resHandler.headers[0].name == "Content-Type");
51     assert(resHandler.headers[0].value == "text/plain");
52     assert(resHandler.headers[1].name == "Content-Length");
53     assert(resHandler.headers[1].value == "3");
54 }
55 
56 @("parseHttpVersion")
57 unittest
58 {
59     assert(parseHttpVersion("FOO") < 0);
60     assert(parseHttpVersion("HTTP/1.") < 0);
61     assert(parseHttpVersion("HTTP/1.12") < 0);
62     assert(parseHttpVersion("HTTP/1.a") < 0);
63     assert(parseHttpVersion("HTTP/2.0") < 0);
64     assert(parseHttpVersion("HTTP/1.00") < 0);
65     assert(parseHttpVersion("HTTP/1.0") == 0);
66     assert(parseHttpVersion("HTTP/1.1") == 1);
67 }
68 
69 version (CI_MAIN)
70 {
71     // workaround for dub not supporting unittests with betterC
72     version (D_BetterC)
73     {
74         extern(C) void main() @trusted {
75             import core.stdc.stdio;
76             static foreach(u; __traits(getUnitTests, httparsed))
77             {
78                 static if (__traits(getAttributes, u).length)
79                     printf("unittest %s:%d | '" ~ __traits(getAttributes, u)[0] ~ "'\n", __traits(getLocation, u)[0].ptr, __traits(getLocation, u)[1]);
80                 else
81                     printf("unittest %s:%d\n", __traits(getLocation, u)[0].ptr, __traits(getLocation, u)[1]);
82                 u();
83             }
84             debug printf("All unit tests have been run successfully.\n");
85         }
86     }
87     else
88     {
89         void main()
90         {
91             version (unittest) {} // run automagically
92             else
93             {
94                 import core.stdc.stdio;
95 
96                 // just a compilation test
97                 auto reqParser = initParser();
98                 auto resParser = initParser();
99 
100                 string data = "GET /foo HTTP/1.1\r\nHost: 127.0.0.1:8090\r\n\r\n";
101                 int res = reqHandler.parseRequest(data);
102                 assert(res == data.length);
103 
104                 data = "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 3\r\n\r\nfoo";
105                 res = resHandler.parseResponse(data);
106                 assert(res == data.length - 3);
107                 () @trusted { printf("Test app works\n"); }();
108             }
109         }
110     }
111 }
112 
113 
114 /// Builds valid char map from the provided ranges of invalid ones
115 bool[256] buildValidCharMap()(string invalidRanges)
116 {
117     assert(invalidRanges.length % 2 == 0, "Uneven ranges");
118     bool[256] res = true;
119 
120     for (int i=0; i < invalidRanges.length; i+=2)
121         for (int j=invalidRanges[i]; j <= invalidRanges[i+1]; ++j)
122             res[j] = false;
123     return res;
124 }
125 
126 @("buildValidCharMap")
127 unittest
128 {
129     string ranges = "\0 \"\"(),,//:@[]{{}}\x7f\xff";
130     assert(buildValidCharMap(ranges) ==
131         cast(bool[])[
132             0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
133             0,1,0,1,1,1,1,1,0,0,1,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,
134             0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,
135             1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0,
136             0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
137             0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
138             0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
139             0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
140         ]);
141 }
142 
143 version (unittest) version = WITH_MSG;
144 else version (CI_MAIN) version = WITH_MSG;
145 
146 version (WITH_MSG)
147 {
148     // define our message content handler
149     struct Header
150     {
151         const(char)[] name;
152         const(char)[] value;
153     }
154 
155     // Just store slices of parsed message header
156     class HttpRequestParserHandler : HttpMessageHandler
157     {
158         @safe pure nothrow @nogc:
159         void onMethod(const(char)[] method) { this.method = method; }
160         void onUri(const(char)[] uri) { this.uri = uri; }
161         int onVersion(const(char)[] ver)
162         {
163             minorVer = parseHttpVersion(ver);
164             return minorVer >= 0 ? 0 : minorVer;
165         }
166         void onHeader(const(char)[] name, const(char)[] value) {
167             this.m_headers[m_headersLength].name = name;
168             this.m_headers[m_headersLength++].value = value;
169         }
170         void onStatus(int status) { this.status = status; }
171         void onStatusMsg(const(char)[] statusMsg) { this.statusMsg = statusMsg; }
172 
173         const(char)[] method;
174         const(char)[] uri;
175         int minorVer;
176         int status;
177         const(char)[] statusMsg;
178 
179         private {
180             Header[32] m_headers;
181             size_t m_headersLength;
182         }
183 
184         Header[] headers() return { return m_headers[0..m_headersLength]; }
185     }
186 
187     enum Test { err, complete, partial }
188 }
189 
190 // Tests from https://github.com/h2o/picohttpparser/blob/master/test.c
191 
192 @("Request")
193 unittest
194 {
195     auto parse(string data, Test test = Test.complete, int additional = 0)
196     {
197         auto parser = new HttpMessageParser(new HttpRequestParserHandler);
198         auto res = parser.parseRequest(data);
199         // if (res < 0) writeln("Err: ", cast(ParserError)(-res));
200         final switch (test)
201         {
202             case Test.err: assert(res < -ParserError.partial); break;
203             case Test.partial: assert(res == -ParserError.partial); break;
204             case Test.complete: assert(res == data.length - additional); break;
205         }
206 
207         return cast(HttpRequestParserHandler) parser.messageHandler();
208     }
209 
210     // simple
211     {
212         auto req = parse("GET / HTTP/1.0\r\n\r\n");
213         assert(req.headers.length == 0);
214         assert(req.method == "GET");
215         assert(req.uri == "/");
216         assert(req.minorVer == 0);
217     }
218 
219     // parse headers
220     {
221         auto req = parse("GET /hoge HTTP/1.1\r\nHost: example.com\r\nCookie: \r\n\r\n");
222         assert(req.method == "GET");
223         assert(req.uri == "/hoge");
224         assert(req.minorVer == 1);
225         assert(req.headers.length == 2);
226         assert(req.headers[0] == Header("Host", "example.com"));
227         assert(req.headers[1] == Header("Cookie", ""));
228     }
229 
230     // multibyte included
231     {
232         auto req = parse("GET /hoge HTTP/1.1\r\nHost: example.com\r\nUser-Agent: \343\201\262\343/1.0\r\n\r\n");
233         assert(req.method == "GET");
234         assert(req.uri == "/hoge");
235         assert(req.minorVer == 1);
236         assert(req.headers.length == 2);
237         assert(req.headers[0] == Header("Host", "example.com"));
238         assert(req.headers[1] == Header("User-Agent", "\343\201\262\343/1.0"));
239     }
240 
241     //multiline
242     {
243         auto req = parse("GET / HTTP/1.0\r\nfoo: \r\nfoo: b\r\n  \tc\r\n\r\n");
244         assert(req.method == "GET");
245         assert(req.uri == "/");
246         assert(req.minorVer == 0);
247         assert(req.headers.length == 3);
248         assert(req.headers[0] == Header("foo", ""));
249         assert(req.headers[1] == Header("foo", "b"));
250         assert(req.headers[2] == Header(null, "  \tc"));
251     }
252 
253     // header name with trailing space
254     parse("GET / HTTP/1.0\r\nfoo : ab\r\n\r\n", Test.err);
255 
256     // incomplete
257     assert(parse("\r", Test.partial).method == null);
258     assert(parse("\r\n", Test.partial).method == null);
259     assert(parse("\r\nGET", Test.partial).method == null);
260     assert(parse("GET", Test.partial).method == null);
261     assert(parse("GET ", Test.partial).method == "GET");
262     assert(parse("GET /", Test.partial).uri == null);
263     assert(parse("GET / ", Test.partial).uri == "/");
264     assert(parse("GET / HTTP/1.1", Test.partial).minorVer == 0);
265     assert(parse("GET / HTTP/1.1\r", Test.partial).minorVer == 1);
266     assert(parse("GET / HTTP/1.1\r\n", Test.partial).minorVer == 1);
267     parse("GET / HTTP/1.0\r\n\r", Test.partial);
268     parse("GET / HTTP/1.0\r\n\r\n", Test.complete);
269     parse(" / HTTP/1.0\r\n\r\n", Test.err); // empty method
270     parse("GET  HTTP/1.0\r\n\r\n", Test.err); // empty request target
271     parse("GET / \r\n\r\n", Test.err); // empty version
272     parse("GET / HTTP/1.0\r\n:a\r\n\r\n", Test.err); // empty header name
273     parse("GET / HTTP/1.0\r\n :a\r\n\r\n", Test.err); // empty header name (space only)
274     parse("G\0T / HTTP/1.0\r\n\r\n", Test.err); // NUL in method
275     parse("G\tT / HTTP/1.0\r\n\r\n", Test.err); // tab in method
276     parse("GET /\x7f HTTP/1.0\r\n\r\n", Test.err); // DEL in uri
277     parse("GET / HTTP/1.0\r\na\0b: c\r\n\r\n", Test.err); // NUL in header name
278     parse("GET / HTTP/1.0\r\nab: c\0d\r\n\r\n", Test.err); // NUL in header value
279     parse("GET / HTTP/1.0\r\na\033b: c\r\n\r\n", Test.err); // CTL in header name
280     parse("GET / HTTP/1.0\r\nab: c\033\r\n\r\n", Test.err); // CTL in header value
281     parse("GET / HTTP/1.0\r\n/: 1\r\n\r\n", Test.err); // invalid char in header value
282     parse("GET   /   HTTP/1.0\r\n\r\n", Test.complete); // multiple spaces between tokens
283 
284     // accept MSB chars
285     {
286         auto res = parse("GET /\xa0 HTTP/1.0\r\nh: c\xa2y\r\n\r\n");
287         assert(res.method == "GET");
288         assert(res.uri == "/\xa0");
289         assert(res.minorVer == 0);
290         assert(res.headers.length == 1);
291         assert(res.headers[0] == Header("h", "c\xa2y"));
292     }
293 
294     parse("GET / HTTP/1.0\r\n\x7b: 1\r\n\r\n", Test.err); // disallow '{'
295 
296     // exclude leading and trailing spaces in header value
297     {
298         auto req = parse("GET / HTTP/1.0\r\nfoo:  a \t \r\n\r\n");
299         assert(req.headers[0].value == "a");
300     }
301 
302     // leave the body intact
303     parse("GET / HTTP/1.0\r\n\r\nfoo bar baz", Test.complete, "foo bar baz".length);
304 
305     // realworld
306     {
307         auto req = parse("GET /cookies HTTP/1.1\r\nHost: 127.0.0.1:8090\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17\r\nAccept-Encoding: gzip,deflate,sdch\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\r\nCookie: name=wookie\r\n\r\n");
308         assert(req.method == "GET");
309         assert(req.uri == "/cookies");
310         assert(req.minorVer == 1);
311         assert(req.headers[0] == Header("Host", "127.0.0.1:8090"));
312         assert(req.headers[1] == Header("Connection", "keep-alive"));
313         assert(req.headers[2] == Header("Cache-Control", "max-age=0"));
314         assert(req.headers[3] == Header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"));
315         assert(req.headers[4] == Header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17"));
316         assert(req.headers[5] == Header("Accept-Encoding", "gzip,deflate,sdch"));
317         assert(req.headers[6] == Header("Accept-Language", "en-US,en;q=0.8"));
318         assert(req.headers[7] == Header("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.3"));
319         assert(req.headers[8] == Header("Cookie", "name=wookie"));
320     }
321 
322     // newline
323     {
324         auto req = parse("GET / HTTP/1.0\nfoo: a\n\n");
325     }
326 }
327 
328 @("Response")
329 // Tests from https://github.com/h2o/picohttpparser/blob/master/test.c
330 unittest
331 {
332     auto parse(string data, Test test = Test.complete, int additional = 0)
333     {
334         auto handler = new HttpRequestParserHandler;
335         auto parser =  new HttpMessageParser(handler);
336 
337         auto res = parser.parseResponse(data);
338         // if (res < 0) writeln("Err: ", cast(ParserError)(-res));
339         final switch (test)
340         {
341             case Test.err: assert(res < -ParserError.partial); break;
342             case Test.partial: assert(res == -ParserError.partial); break;
343             case Test.complete: assert(res == data.length - additional); break;
344         }
345 
346         return handler;
347     }
348 
349     // simple
350     {
351         auto res = parse("HTTP/1.0 200 OK\r\n\r\n");
352         assert(res.headers.length == 0);
353         assert(res.status == 200);
354         assert(res.minorVer == 0);
355         assert(res.statusMsg == "OK");
356     }
357 
358     parse("HTTP/1.0 200 OK\r\n\r", Test.partial); // partial
359 
360     // parse headers
361     {
362         auto res = parse("HTTP/1.1 200 OK\r\nHost: example.com\r\nCookie: \r\n\r\n");
363         assert(res.headers.length == 2);
364         assert(res.minorVer == 1);
365         assert(res.status == 200);
366         assert(res.statusMsg == "OK");
367         assert(res.headers[0] == Header("Host", "example.com"));
368         assert(res.headers[1] == Header("Cookie", ""));
369     }
370 
371     // parse multiline
372     {
373         auto res = parse("HTTP/1.0 200 OK\r\nfoo: \r\nfoo: b\r\n  \tc\r\n\r\n");
374         assert(res.headers.length == 3);
375         assert(res.minorVer == 0);
376         assert(res.status == 200);
377         assert(res.statusMsg == "OK");
378         assert(res.headers[0] == Header("foo", ""));
379         assert(res.headers[1] == Header("foo", "b"));
380         assert(res.headers[2] == Header(null, "  \tc"));
381     }
382 
383     // internal server error
384     {
385         auto res = parse("HTTP/1.0 500 Internal Server Error\r\n\r\n");
386         assert(res.headers.length == 0);
387         assert(res.minorVer == 0);
388         assert(res.status == 500);
389         assert(res.statusMsg == "Internal Server Error");
390     }
391 
392     parse("H", Test.partial); // incomplete 1
393     parse("HTTP/1.", Test.partial); // incomplete 2
394     assert(parse("HTTP/1.1", Test.partial).minorVer == 0); // incomplete 3 - differs from picohttpparser as we don't parse exact version
395     assert(parse("HTTP/1.1 ", Test.partial).minorVer == 1); // incomplete 4
396     parse("HTTP/1.1 2", Test.partial); // incomplete 5
397     assert(parse("HTTP/1.1 200", Test.partial).status == 0); // incomplete 6
398     assert(parse("HTTP/1.1 200 ", Test.partial).status == 200); // incomplete 7
399     assert(parse("HTTP/1.1 200\r", Test.partial).status == 200); // incomplete 7.1
400     parse("HTTP/1.1 200 O", Test.partial); // incomplete 8
401     assert(parse("HTTP/1.1 200 OK\r", Test.partial).statusMsg == "OK"); // incomplete 9 - differs from picohttpparser
402     assert(parse("HTTP/1.1 200 OK\r\n", Test.partial).statusMsg == "OK"); // incomplete 10
403     assert(parse("HTTP/1.1 200 OK\n", Test.partial).statusMsg == "OK"); // incomplete 11
404     assert(parse("HTTP/1.1 200 OK\r\nA: 1\r", Test.partial).headers.length == 0); // incomplete 11
405     parse("HTTP/1.1   200   OK\r\n\r\n", Test.complete); // multiple spaces between tokens
406 
407     // incomplete 12
408     {
409         auto res = parse("HTTP/1.1 200 OK\r\nA: 1\r\n", Test.partial);
410         assert(res.headers.length == 1);
411         assert(res.headers[0] == Header("A", "1"));
412     }
413 
414     // slowloris (incomplete)
415     {
416         auto parser =  new HttpMessageParser(new HttpRequestParserHandler);
417         assert(parser.parseResponse("HTTP/1.0 200 OK\r\n") == -ParserError.partial);
418         assert(parser.parseResponse("HTTP/1.0 200 OK\r\n\r") == -ParserError.partial);
419         assert(parser.parseResponse("HTTP/1.0 200 OK\r\n\r\nblabla") == "HTTP/1.0 200 OK\r\n\r\n".length);
420     }
421 
422     parse("HTTP/1. 200 OK\r\n\r\n", Test.err); // invalid http version
423     parse("HTTP/1.2z 200 OK\r\n\r\n", Test.err); // invalid http version 2
424     parse("HTTP/1.1  OK\r\n\r\n", Test.err); // no status code
425 
426     assert(parse("HTTP/1.1 200\r\n\r\n").statusMsg == ""); // accept missing trailing whitespace in status-line
427     parse("HTTP/1.1 200X\r\n\r\n", Test.err); // garbage after status 1
428     parse("HTTP/1.1 200X \r\n\r\n", Test.err); // garbage after status 2
429     parse("HTTP/1.1 200X OK\r\n\r\n", Test.err); // garbage after status 3
430 
431     assert(parse("HTTP/1.1 200 OK\r\nbar: \t b\t \t\r\n\r\n").headers[0].value == "b"); // exclude leading and trailing spaces in header value
432 }
433 
434 @("Incremental")
435 unittest
436 {
437     string req = "GET /cookies HTTP/1.1\r\nHost: 127.0.0.1:8090\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17\r\nAccept-Encoding: gzip,deflate,sdch\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\r\nCookie: name=wookie\r\n\r\n";
438     auto handler = new HttpRequestParserHandler;
439     auto parser =  new HttpMessageParser(handler);
440     uint parsed;
441     auto res = parser.parseRequest(req[0.."GET /cookies HTTP/1.1\r\nHost: 127.0.0.1:8090\r\nConn".length], parsed);
442     assert(res == -ParserError.partial);
443     assert(handler.method == "GET");
444     assert(handler.uri == "/cookies");
445     assert(handler.minorVer == 1);
446     assert(handler.headers.length == 1);
447     assert(handler.headers[0] == Header("Host", "127.0.0.1:8090"));
448 
449     res = parser.parseRequest(req, parsed);
450     assert(res == req.length);
451     assert(handler.method == "GET");
452     assert(handler.uri == "/cookies");
453     assert(handler.minorVer == 1);
454     assert(handler.headers[0] == Header("Host", "127.0.0.1:8090"));
455     assert(handler.headers[1] == Header("Connection", "keep-alive"));
456     assert(handler.headers[2] == Header("Cache-Control", "max-age=0"));
457     assert(handler.headers[3] == Header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"));
458     assert(handler.headers[4] == Header("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17"));
459     assert(handler.headers[5] == Header("Accept-Encoding", "gzip,deflate,sdch"));
460     assert(handler.headers[6] == Header("Accept-Language", "en-US,en;q=0.8"));
461     assert(handler.headers[7] == Header("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.3"));
462     assert(handler.headers[8] == Header("Cookie", "name=wookie"));
463 }