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 }