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 }