1 module hunt.http.codec.http.encode.HttpGenerator; 2 3 import hunt.http.codec.http.model; 4 // import hunt.http.codec.http.model.HttpField; 5 // import hunt.http.codec.http.model.HttpFields; 6 // import hunt.http.codec.http.model.HttpMethod; 7 // import hunt.http.codec.http.model.HttpHeader; 8 // import hunt.http.codec.http.model.HttpTokens; 9 // import hunt.http.codec.http.model.HttpVersion; 10 // import hunt.http.codec.http.model.MetaData; 11 import hunt.http.codec.http.hpack.HpackEncoder; 12 13 import hunt.http.environment; 14 import hunt.container; 15 import hunt.datetime; 16 import hunt.lang.exception; 17 import hunt.lang.common; 18 import hunt.logging; 19 import hunt.string; 20 21 import core.time; 22 import std.conv; 23 import std.format; 24 import std.string; 25 26 27 /** 28 * HttpGenerator. Builds HTTP Messages. 29 * <p> 30 * If the system property "org.fireflysource.http.HttpGenerator.STRICT" is set 31 * to true, then the generator will strictly pass on the exact strings received 32 * from methods and header fields. Otherwise a fast case insensitive string 33 * lookup is used that may alter the case and white space of some 34 * methods/headers 35 */ 36 class HttpGenerator { 37 // static bool __STRICT = bool.getBoolean("hunt.http.codec.http.encode.HttpGenerator.STRICT"); 38 39 private enum byte[] __colon_space = [':', ' ']; 40 __gshared HttpResponse CONTINUE_100_INFO; 41 __gshared HttpResponse PROGRESS_102_INFO; 42 __gshared HttpResponse RESPONSE_500_INFO; 43 44 45 // states 46 enum State { 47 START, COMMITTED, COMPLETING, COMPLETING_1XX, END 48 } 49 50 enum Result { 51 NEED_CHUNK, // Need a small chunk buffer of CHUNK_SIZE 52 NEED_INFO, // Need the request/response metadata info 53 NEED_HEADER, // Need a buffer to build HTTP headers into 54 NEED_CHUNK_TRAILER, // Need a large chunk buffer for last chunk and trailers 55 FLUSH, // The buffers previously generated should be flushed 56 CONTINUE, // Continue generating the message 57 SHUTDOWN_OUT, // Need EOF to be signaled 58 DONE // Message generation complete 59 } 60 61 // other statics 62 static int CHUNK_SIZE = 12; 63 64 private State _state = State.START; 65 private EndOfContent _endOfContent = EndOfContent.UNKNOWN_CONTENT; 66 67 private long _contentPrepared = 0; 68 private bool _noContentResponse = false; 69 private bool _persistent = false; 70 private Supplier!HttpFields _trailers = null; 71 72 private int _send; 73 private __gshared int SEND_SERVER = 0x01; 74 private __gshared int SEND_XPOWEREDBY = 0x02; 75 private __gshared bool[string] __assumedContentMethods; // = new ArrayTrie<>(8); 76 77 shared static this() { 78 __assumedContentMethods[HttpMethod.POST.asString()] = true; 79 __assumedContentMethods[HttpMethod.PUT.asString()] = true; 80 CONTINUE_100_INFO = new HttpResponse(HttpVersion.HTTP_1_1, 100, null, null, -1); 81 PROGRESS_102_INFO = new HttpResponse(HttpVersion.HTTP_1_1, 102, null, null, -1); 82 HttpFields hf = new HttpFields(); 83 hf.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE); 84 85 RESPONSE_500_INFO = new HttpResponse(HttpVersion.HTTP_1_1, HttpStatus.INTERNAL_SERVER_ERROR_500, null, hf, 0); 86 } 87 88 /* ------------------------------------------------------------------------------- */ 89 static void setFireflyVersion(string serverVersion) { 90 SEND[SEND_SERVER] = StringUtils.getBytes("Server: " ~ serverVersion ~ "\015\012"); 91 SEND[SEND_XPOWEREDBY] = StringUtils.getBytes("X-Powered-By: " ~ serverVersion ~ "\015\012"); 92 SEND[SEND_SERVER | SEND_XPOWEREDBY] = StringUtils.getBytes("Server: " ~ serverVersion ~ "\015\012X-Powered-By: " ~ 93 serverVersion ~ "\015\012"); 94 } 95 96 /* ------------------------------------------------------------------------------- */ 97 // data 98 private bool _needCRLF = false; 99 private MonoTime startTime; 100 101 /* ------------------------------------------------------------------------------- */ 102 this() { 103 this(false, false); 104 } 105 106 /* ------------------------------------------------------------------------------- */ 107 this(bool sendServerVersion, bool sendXPoweredBy) { 108 _send = (sendServerVersion ? SEND_SERVER : 0) | (sendXPoweredBy ? SEND_XPOWEREDBY : 0); 109 } 110 111 /* ------------------------------------------------------------------------------- */ 112 void reset() { 113 _state = State.START; 114 _endOfContent = EndOfContent.UNKNOWN_CONTENT; 115 _noContentResponse = false; 116 _persistent = false; 117 _contentPrepared = 0; 118 _needCRLF = false; 119 _trailers = null; 120 } 121 122 /* ------------------------------------------------------------ */ 123 State getState() { 124 return _state; 125 } 126 127 /* ------------------------------------------------------------ */ 128 bool isState(State state) { 129 return _state == state; 130 } 131 132 /* ------------------------------------------------------------ */ 133 bool isIdle() { 134 return _state == State.START; 135 } 136 137 /* ------------------------------------------------------------ */ 138 bool isEnd() { 139 return _state == State.END; 140 } 141 142 /* ------------------------------------------------------------ */ 143 bool isCommitted() { 144 return _state >= State.COMMITTED; 145 } 146 147 /* ------------------------------------------------------------ */ 148 bool isChunking() { 149 return _endOfContent == EndOfContent.CHUNKED_CONTENT; 150 } 151 152 /* ------------------------------------------------------------ */ 153 bool isNoContent() { 154 return _noContentResponse; 155 } 156 157 /* ------------------------------------------------------------ */ 158 void setPersistent(bool persistent) { 159 _persistent = persistent; 160 } 161 162 /* ------------------------------------------------------------ */ 163 164 /** 165 * @return true if known to be persistent 166 */ 167 bool isPersistent() { 168 return _persistent; 169 } 170 171 /* ------------------------------------------------------------ */ 172 bool isWritten() { 173 return _contentPrepared > 0; 174 } 175 176 /* ------------------------------------------------------------ */ 177 long getContentPrepared() { 178 return _contentPrepared; 179 } 180 181 /* ------------------------------------------------------------ */ 182 void abort() { 183 _persistent = false; 184 _state = State.END; 185 _endOfContent = EndOfContent.UNKNOWN_CONTENT; 186 } 187 188 /* ------------------------------------------------------------ */ 189 Result generateRequest(HttpRequest metaData, ByteBuffer header, 190 ByteBuffer chunk, ByteBuffer content, bool last) { 191 192 switch (_state) { 193 case State.START: { 194 if (metaData is null) 195 return Result.NEED_INFO; 196 197 if (header is null) 198 return Result.NEED_HEADER; 199 200 // If we have not been told our persistence, set the default 201 // if (_persistent == null) 202 { 203 // HttpVersion v = HttpVersion.HTTP_1_0; 204 _persistent = metaData.getHttpVersion() > HttpVersion.HTTP_1_0; 205 if (!_persistent && HttpMethod.CONNECT.isSame(metaData.getMethod())) 206 _persistent = true; 207 } 208 209 // prepare the header 210 int pos = BufferUtils.flipToFill(header); 211 try { 212 // generate ResponseLine 213 generateRequestLine(metaData, header); 214 215 if (metaData.getHttpVersion() == HttpVersion.HTTP_0_9) 216 throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, "HTTP/0.9 not supported"); 217 218 generateHeaders(metaData, header, content, last); 219 enum string continueString = HttpHeaderValue.CONTINUE.asString(); 220 bool expect100 = metaData.getFields().contains(HttpHeader.EXPECT, continueString); 221 222 if (expect100) { 223 _state = State.COMMITTED; 224 } else { 225 // handle the content. 226 int len = content.remaining(); // BufferUtils.length(content); 227 if (len > 0) { 228 _contentPrepared += len; 229 if (isChunking()) 230 prepareChunk(header, len); 231 } 232 _state = last ? State.COMPLETING : State.COMMITTED; 233 } 234 235 return Result.FLUSH; 236 } catch (BadMessageException e) { 237 throw e; 238 } catch (BufferOverflowException e) { 239 throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Request header too large", e); 240 } catch (Exception e) { 241 throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, cast(string)e.message(), e); 242 } finally { 243 BufferUtils.flipToFlush(header, pos); 244 } 245 } 246 247 case State.COMMITTED: { 248 return committed(chunk, content, last); 249 } 250 251 case State.COMPLETING: { 252 return completing(chunk, content); 253 } 254 255 case State.END: 256 if (BufferUtils.hasContent(content)) { 257 version(HUNT_DEBUG) { 258 tracef("discarding content in COMPLETING"); 259 } 260 BufferUtils.clear(content); 261 } 262 return Result.DONE; 263 264 default: 265 throw new IllegalStateException(""); 266 } 267 } 268 269 private Result committed(ByteBuffer chunk, ByteBuffer content, bool last) { 270 int len = BufferUtils.length(content); 271 272 // handle the content. 273 if (len > 0) { 274 if (isChunking()) { 275 if (chunk is null) 276 return Result.NEED_CHUNK; 277 BufferUtils.clearToFill(chunk); 278 prepareChunk(chunk, len); 279 BufferUtils.flipToFlush(chunk, 0); 280 } 281 _contentPrepared += len; 282 } 283 284 if (last) { 285 _state = State.COMPLETING; 286 return len > 0 ? Result.FLUSH : Result.CONTINUE; 287 } 288 return len > 0 ? Result.FLUSH : Result.DONE; 289 } 290 291 private Result completing(ByteBuffer chunk, ByteBuffer content) { 292 version(HUNT_METRIC) { 293 scope(exit) { 294 Duration timeElapsed = MonoTime.currTime - startTime; 295 warningf("generating completed in: %d microseconds", 296 timeElapsed.total!(TimeUnit.Microsecond)()); 297 } 298 } 299 300 if (BufferUtils.hasContent(content)) { 301 version(HUNT_DEBUG) 302 tracef("discarding content in COMPLETING"); 303 BufferUtils.clear(content); 304 } 305 306 if (isChunking()) { 307 if (_trailers != null) { 308 // Do we need a chunk buffer? 309 if (chunk is null || chunk.capacity() <= CHUNK_SIZE) 310 return Result.NEED_CHUNK_TRAILER; 311 312 HttpFields trailers = _trailers(); 313 314 if (trailers !is null) { 315 // Write the last chunk 316 BufferUtils.clearToFill(chunk); 317 generateTrailers(chunk, trailers); 318 BufferUtils.flipToFlush(chunk, 0); 319 _endOfContent = EndOfContent.UNKNOWN_CONTENT; 320 return Result.FLUSH; 321 } 322 } 323 324 // Do we need a chunk buffer? 325 if (chunk is null) 326 return Result.NEED_CHUNK; 327 328 // Write the last chunk 329 BufferUtils.clearToFill(chunk); 330 prepareChunk(chunk, 0); 331 BufferUtils.flipToFlush(chunk, 0); 332 _endOfContent = EndOfContent.UNKNOWN_CONTENT; 333 return Result.FLUSH; 334 } 335 336 _state = State.END; 337 return _persistent ? Result.DONE : Result.SHUTDOWN_OUT; 338 339 } 340 341 /* ------------------------------------------------------------ */ 342 Result generateResponse(HttpResponse metaData, bool head, ByteBuffer header, 343 ByteBuffer chunk, ByteBuffer content, bool last){ 344 switch (_state) { 345 case State.START: { 346 if (metaData is null) 347 return Result.NEED_INFO; 348 HttpVersion ver = metaData.getHttpVersion(); 349 if (ver == HttpVersion.Null) { 350 throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, "No version"); 351 } 352 353 version(HUNT_METRIC) { 354 startTime = MonoTime.currTime; 355 debug info("generating response..."); 356 } 357 switch (ver.getVersion()) { 358 case HttpVersion.HTTP_1_0.getVersion(): 359 _persistent = false; 360 break; 361 362 case HttpVersion.HTTP_1_1.getVersion(): 363 _persistent = true; 364 break; 365 366 default: 367 _persistent = false; 368 _endOfContent = EndOfContent.EOF_CONTENT; 369 if (BufferUtils.hasContent(content)) 370 _contentPrepared += content.remaining(); 371 _state = last ? State.COMPLETING : State.COMMITTED; 372 return Result.FLUSH; 373 } 374 375 // Do we need a response header 376 if (header is null) 377 return Result.NEED_HEADER; 378 379 // prepare the header 380 int pos = BufferUtils.flipToFill(header); 381 try { 382 // generate ResponseLine 383 generateResponseLine(metaData, header); 384 385 // Handle 1xx and no content responses 386 int status = metaData.getStatus(); 387 if (status >= 100 && status < 200) { 388 _noContentResponse = true; 389 390 if (status != HttpStatus.SWITCHING_PROTOCOLS_101) { 391 header.put(HttpTokens.CRLF); 392 _state = State.COMPLETING_1XX; 393 return Result.FLUSH; 394 } 395 } else if (status == HttpStatus.NO_CONTENT_204 || status == HttpStatus.NOT_MODIFIED_304) { 396 _noContentResponse = true; 397 } 398 399 generateHeaders(metaData, header, content, last); 400 401 // handle the content. 402 int len = BufferUtils.length(content); 403 if (len > 0) { 404 _contentPrepared += len; 405 if (isChunking() && !head) 406 prepareChunk(header, len); 407 } 408 _state = last ? State.COMPLETING : State.COMMITTED; 409 } catch (BadMessageException e) { 410 throw e; 411 } catch (BufferOverflowException e) { 412 throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Request header too large", e); 413 } catch (Exception e) { 414 throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, cast(string)e.message(), e); 415 } finally { 416 BufferUtils.flipToFlush(header, pos); 417 } 418 419 version(HUNT_METRIC) { 420 // debug infof("_state: %s", _state); 421 if(_state == State.COMMITTED) { 422 Duration timeElapsed = MonoTime.currTime - startTime; 423 warningf("comitted in: %d microseconds", 424 timeElapsed.total!(TimeUnit.Microsecond)()); 425 } 426 } 427 return Result.FLUSH; 428 } 429 430 case State.COMMITTED: { 431 return committed(chunk, content, last); 432 } 433 434 case State.COMPLETING_1XX: { 435 reset(); 436 return Result.DONE; 437 } 438 439 case State.COMPLETING: { 440 return completing(chunk, content); 441 } 442 443 case State.END: 444 info("444444"); 445 if (BufferUtils.hasContent(content)) { 446 version(HUNT_DEBUG) { 447 tracef("discarding content in COMPLETING"); 448 } 449 BufferUtils.clear(content); 450 } 451 return Result.DONE; 452 453 default: { 454 string msg = format("bad generator state: %s", _state); 455 warning(msg); 456 throw new IllegalStateException(msg); 457 } 458 } 459 } 460 461 /* ------------------------------------------------------------ */ 462 private void prepareChunk(ByteBuffer chunk, int remaining) { 463 // if we need CRLF add this to header 464 if (_needCRLF) 465 BufferUtils.putCRLF(chunk); 466 467 // Add the chunk size to the header 468 if (remaining > 0) { 469 BufferUtils.putHexInt(chunk, remaining); 470 BufferUtils.putCRLF(chunk); 471 _needCRLF = true; 472 } else { 473 chunk.put(LAST_CHUNK); 474 _needCRLF = false; 475 } 476 } 477 478 /* ------------------------------------------------------------ */ 479 private void generateTrailers(ByteBuffer buffer, HttpFields trailer) { 480 // if we need CRLF add this to header 481 if (_needCRLF) 482 BufferUtils.putCRLF(buffer); 483 484 // Add the chunk size to the header 485 buffer.put(ZERO_CHUNK); 486 487 int n = trailer.size(); 488 for (int f = 0; f < n; f++) { 489 HttpField field = trailer.getField(f); 490 string v = field.getValue(); 491 if (v == null || v.length == 0) 492 continue; // rfc7230 does not allow no value 493 494 putTo(field, buffer); 495 } 496 497 BufferUtils.putCRLF(buffer); 498 } 499 500 /* ------------------------------------------------------------ */ 501 private void generateRequestLine(HttpRequest request, ByteBuffer header) { 502 header.put(StringUtils.getBytes(request.getMethod())); 503 header.put(cast(byte) ' '); 504 header.put(StringUtils.getBytes(request.getURIString())); 505 header.put(cast(byte) ' '); 506 header.put(request.getHttpVersion().toBytes()); 507 header.put(HttpTokens.CRLF); 508 } 509 510 /* ------------------------------------------------------------ */ 511 private void generateResponseLine(HttpResponse response, ByteBuffer header) { 512 // Look for prepared response line 513 int status = response.getStatus(); 514 PreparedResponse preprepared = status < __preprepared.length ? __preprepared[status] : null; 515 string reason = response.getReason(); 516 if (preprepared !is null) { 517 if (reason == null) 518 header.put(preprepared._responseLine); 519 else { 520 header.put(preprepared._schemeCode); 521 header.put(getReasonBytes(reason)); 522 header.put(HttpTokens.CRLF); 523 } 524 } else // generate response line 525 { 526 header.put(HTTP_1_1_SPACE); 527 header.put(cast(byte) ('0' + status / 100)); 528 header.put(cast(byte) ('0' + (status % 100) / 10)); 529 header.put(cast(byte) ('0' + (status % 10))); 530 header.put(cast(byte) ' '); 531 if (reason == null) { 532 header.put(cast(byte) ('0' + status / 100)); 533 header.put(cast(byte) ('0' + (status % 100) / 10)); 534 header.put(cast(byte) ('0' + (status % 10))); 535 } else 536 header.put(getReasonBytes(reason)); 537 header.put(HttpTokens.CRLF); 538 } 539 } 540 541 /* ------------------------------------------------------------ */ 542 private byte[] getReasonBytes(string reason) { 543 if (reason.length > 1024) 544 reason = reason.substring(0, 1024); 545 byte[] _bytes = StringUtils.getBytes(reason); 546 547 for (size_t i = _bytes.length; i-- > 0; ) 548 if (_bytes[i] == '\r' || _bytes[i] == '\n') 549 _bytes[i] = '?'; 550 return _bytes; 551 } 552 553 /* ------------------------------------------------------------ */ 554 private void generateHeaders(MetaData metaData, ByteBuffer header, ByteBuffer content, bool last) { 555 HttpRequest request = cast(HttpRequest) metaData; 556 HttpResponse response = cast(HttpResponse) metaData; 557 558 // version(HUNT_DEBUG) { 559 // tracef("Header fields:\n%s", metaData.getFields().toString()); 560 // tracef("generateHeaders %s last=%s content=%s", metaData.toString(), last, BufferUtils.toDetailString(content)); 561 // } 562 563 // default field values 564 int send = _send; 565 HttpField transfer_encoding = null; 566 bool http11 = metaData.getHttpVersion() == HttpVersion.HTTP_1_1; 567 bool close = false; 568 _trailers = http11 ? metaData.getTrailerSupplier() : null; 569 bool chunked_hint = _trailers != null; 570 bool content_type = false; 571 long content_length = metaData.getContentLength(); 572 bool content_length_field = false; 573 574 // Generate fields 575 HttpFields fields = metaData.getFields(); 576 if (fields !is null) { 577 for (int f = 0; f < fields.size(); f++) { 578 HttpField field = fields.getField(f); 579 string v = field.getValue(); 580 if (v == null || v.length == 0) 581 continue; // rfc7230 does not allow no value 582 583 HttpHeader h = field.getHeader(); 584 if (h == HttpHeader.Null) { 585 putTo(field, header); 586 } 587 else { 588 if(h == HttpHeader.CONTENT_LENGTH) { 589 if (content_length < 0) 590 content_length = field.getLongValue(); 591 else if (content_length != field.getLongValue()) { 592 throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, 593 format("Incorrect Content-Length %d!=%d", 594 content_length, field.getLongValue())); 595 } 596 content_length_field = true; 597 } 598 else if(h == HttpHeader.CONTENT_TYPE) { 599 // write the field to the header 600 content_type = true; 601 putTo(field, header); 602 } 603 else if(h == HttpHeader.TRANSFER_ENCODING) { 604 if (http11) { 605 // Don't add yet, treat this only as a hint that there is content 606 // with a preference to chunk if we can 607 transfer_encoding = field; 608 chunked_hint = field.contains(HttpHeaderValue.CHUNKED.asString()); 609 } 610 } 611 else if(h == HttpHeader.CONNECTION) { 612 putTo(field, header); 613 if (field.contains(HttpHeaderValue.CLOSE.asString())) { 614 close = true; 615 _persistent = false; 616 } 617 618 if (!http11 && field.contains(HttpHeaderValue.KEEP_ALIVE.asString())) { 619 _persistent = true; 620 } 621 } 622 else if(h == HttpHeader.SERVER) { 623 send = send & ~SEND_SERVER; 624 putTo(field, header); 625 } 626 else 627 putTo(field, header); 628 } 629 } 630 } 631 632 // Can we work out the content length? 633 if (last && content_length < 0 && _trailers == null) 634 content_length = _contentPrepared + BufferUtils.length(content); 635 636 // Calculate how to end _content and connection, _content length and transfer encoding 637 // settings from http://tools.ietf.org/html/rfc7230#section-3.3.3 638 bool assumed_content_request = false; 639 if(request !is null) { 640 string currentMethold = request.getMethod(); 641 bool* itemPtr = currentMethold in __assumedContentMethods; 642 if(itemPtr !is null ) 643 assumed_content_request = *itemPtr; 644 } 645 646 bool assumed_content = assumed_content_request || content_type || chunked_hint; 647 bool nocontent_request = request !is null && content_length <= 0 && !assumed_content; 648 649 // If the message is known not to have content 650 if (_noContentResponse || nocontent_request) { 651 // We don't need to indicate a body length 652 _endOfContent = EndOfContent.NO_CONTENT; 653 654 // But it is an error if there actually is content 655 if (_contentPrepared > 0 || content_length > 0) { 656 if (_contentPrepared == 0 && last) { 657 // TODO discard content for backward compatibility with 9.3 releases 658 // TODO review if it is still needed in 9.4 or can we just throw. 659 content.clear(); 660 content_length = 0; 661 } else 662 throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Content for no content response"); 663 } 664 } 665 // Else if we are HTTP/1.1 and the content length is unknown and we are either persistent 666 // or it is a request with content (which cannot EOF) or the app has requested chunking 667 else if (http11 && content_length < 0 && (_persistent || assumed_content_request || chunked_hint)) { 668 // we use chunking 669 _endOfContent = EndOfContent.CHUNKED_CONTENT; 670 671 // try to use user supplied encoding as it may have other values. 672 if (transfer_encoding is null) 673 header.put(TRANSFER_ENCODING_CHUNKED); 674 else if (transfer_encoding.toString().endsWith(HttpHeaderValue.CHUNKED.toString())) { 675 putTo(transfer_encoding, header); 676 transfer_encoding = null; 677 } else if (!chunked_hint) { 678 putTo(new HttpField(HttpHeader.TRANSFER_ENCODING, transfer_encoding.getValue() ~ ",chunked"), header); 679 transfer_encoding = null; 680 } else 681 throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Bad Transfer-Encoding"); 682 } 683 // Else if we known the content length and are a request or a persistent response, 684 else if (content_length >= 0 && (request !is null || _persistent)) { 685 // Use the content length 686 _endOfContent = EndOfContent.CONTENT_LENGTH; 687 putContentLength(header, content_length); 688 } 689 // Else if we are a response 690 else if (response !is null) { 691 // We must use EOF - even if we were trying to be persistent 692 _endOfContent = EndOfContent.EOF_CONTENT; 693 _persistent = false; 694 if (content_length >= 0 && (content_length > 0 || assumed_content || content_length_field)) 695 putContentLength(header, content_length); 696 697 if (http11 && !close) 698 header.put(CONNECTION_CLOSE); 699 } 700 // Else we must be a request 701 else { 702 // with no way to indicate body length 703 throw new BadMessageException(HttpStatus.INTERNAL_SERVER_ERROR_500, "Unknown content length for request"); 704 } 705 706 // version(HUNT_DEBUG) { 707 // trace("End Of Content: ", _endOfContent.to!string()); 708 // } 709 // Add transfer encoding if it is not chunking 710 if (transfer_encoding !is null) { 711 if (chunked_hint) { 712 string v = transfer_encoding.getValue(); 713 int c = cast(int)v.lastIndexOf(','); 714 if (c > 0 && v.lastIndexOf(HttpHeaderValue.CHUNKED.toString(), c) > c) 715 putTo(new HttpField(HttpHeader.TRANSFER_ENCODING, v.substring(0, c).strip()), header); 716 } else { 717 putTo(transfer_encoding, header); 718 } 719 } 720 721 // Send server? 722 int status = response !is null ? response.getStatus() : -1; 723 if (status > 199) 724 header.put(SEND[send]); 725 726 // end the header. 727 header.put(HttpTokens.CRLF); 728 } 729 730 /* ------------------------------------------------------------------------------- */ 731 private static void putContentLength(ByteBuffer header, long contentLength) { 732 if (contentLength == 0) 733 header.put(CONTENT_LENGTH_0); 734 else { 735 header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace()); 736 BufferUtils.putDecLong(header, contentLength); 737 header.put(HttpTokens.CRLF); 738 } 739 } 740 741 /* ------------------------------------------------------------------------------- */ 742 static byte[] getReasonBuffer(int code) { 743 PreparedResponse status = code < __preprepared.length ? __preprepared[code] : null; 744 if (status !is null) 745 return status._reason; 746 return null; 747 } 748 749 /* ------------------------------------------------------------------------------- */ 750 override 751 string toString() { 752 return format("%s@%x{s=%s}", typeof(this).stringof, 753 toHash(), 754 _state); 755 } 756 757 /* ------------------------------------------------------------------------------- */ 758 /* ------------------------------------------------------------------------------- */ 759 /* ------------------------------------------------------------------------------- */ 760 // common _content 761 private enum byte[] ZERO_CHUNK = ['0', '\015', '\012']; 762 private enum byte[] LAST_CHUNK = ['0', '\015', '\012', '\015', '\012']; 763 private __gshared byte[] CONTENT_LENGTH_0; // = StringUtils.getBytes("Content-Length: 0\015\012"); 764 private __gshared byte[] CONNECTION_CLOSE; // = StringUtils.getBytes("Connection: close\015\012"); 765 private __gshared byte[] HTTP_1_1_SPACE; // = StringUtils.getBytes(HttpVersion.HTTP_1_1.toString() ~ " "); 766 private __gshared byte[] TRANSFER_ENCODING_CHUNKED; // = StringUtils.getBytes("Transfer-Encoding: chunked\015\012"); 767 private __gshared byte[][] SEND; 768 769 /* ------------------------------------------------------------------------------- */ 770 /* ------------------------------------------------------------------------------- */ 771 /* ------------------------------------------------------------------------------- */ 772 // Build cache of response lines for status 773 private static class PreparedResponse { 774 byte[] _reason; 775 byte[] _schemeCode; 776 byte[] _responseLine; 777 } 778 779 private __gshared PreparedResponse[] __preprepared; // = new PreparedResponse[HttpStatus.MAX_CODE + 1]; 780 781 shared static this() { 782 CONTENT_LENGTH_0 = StringUtils.getBytes("Content-Length: 0\015\012"); 783 CONNECTION_CLOSE = StringUtils.getBytes("Connection: close\015\012"); 784 HTTP_1_1_SPACE = StringUtils.getBytes(HttpVersion.HTTP_1_1.toString() ~ " "); 785 TRANSFER_ENCODING_CHUNKED = StringUtils.getBytes("Transfer-Encoding: chunked\015\012"); 786 SEND = [ 787 new byte[0], 788 StringUtils.getBytes("Server: Hunt(" ~ Version ~ ")\015\012"), 789 StringUtils.getBytes("X-Powered-By: Hunt(" ~ Version ~ ")\015\012"), 790 StringUtils.getBytes("Server: Hunt(" ~ Version ~ ")\015\012X-Powered-By: Hunt(" ~ Version ~ ")\015\012") 791 ]; 792 793 __preprepared = new PreparedResponse[HttpStatus.MAX_CODE + 1]; 794 795 string versionString = HttpVersion.HTTP_1_1.toString(); 796 int versionLength = cast(int)versionString.length; 797 798 for (int i = 0; i < __preprepared.length; i++) { 799 HttpStatus.Code code = HttpStatus.getCode(i); 800 if (code == HttpStatus.Code.Null) 801 continue; 802 string reason = code.getMessage(); 803 byte[] line = new byte[versionLength + 5 + reason.length + 2]; 804 line[0 .. versionLength] = cast(byte[])versionString[0 .. $]; 805 // HttpVersion.HTTP_1_1.toBuffer().get(line, 0, versionLength); 806 807 line[versionLength + 0] = ' '; 808 line[versionLength + 1] = cast(byte) ('0' + i / 100); 809 line[versionLength + 2] = cast(byte)('0' + (i % 100) / 10); 810 line[versionLength + 3] = cast(byte)('0' + (i % 10)); 811 line[versionLength + 4] = ' '; 812 for (int j = 0; j < reason.length; j++) 813 line[versionLength + 5 + j] = cast(byte)reason.charAt(j); 814 line[versionLength + 5 + reason.length] = HttpTokens.CARRIAGE_RETURN; 815 line[versionLength + 6 + reason.length] = HttpTokens.LINE_FEED; 816 817 __preprepared[i] = new PreparedResponse(); 818 __preprepared[i]._schemeCode = line[0 .. versionLength + 5].dup; 819 __preprepared[i]._reason = line[versionLength + 5 .. line.length - 2]; 820 __preprepared[i]._responseLine = line; 821 } 822 } 823 824 private static void putSanitisedName(string s, ByteBuffer buffer) { 825 int l = cast(int)s.length; 826 for (int i = 0; i < l; i++) { 827 char c = s[i]; 828 829 if (c < 0 || c > 0xff || c == '\r' || c == '\n' || c == ':') 830 buffer.put(cast(byte) '?'); 831 else 832 buffer.put(cast(byte) (0xff & c)); 833 } 834 } 835 836 private static void putSanitisedValue(string s, ByteBuffer buffer) { 837 int l = cast(int)s.length; 838 for (int i = 0; i < l; i++) { 839 char c = s[i]; 840 841 if (c < 0 || c > 0xff || c == '\r' || c == '\n') 842 buffer.put(cast(byte) ' '); 843 else 844 buffer.put(cast(byte) (0xff & c)); 845 } 846 } 847 848 static void putTo(HttpField field, ByteBuffer bufferInFillMode) { 849 if (typeid(field) == typeid(PreEncodedHttpField)) { 850 (cast(PreEncodedHttpField) field).putTo(bufferInFillMode, HttpVersion.HTTP_1_0); 851 } else { 852 HttpHeader header = field.getHeader(); 853 if (header != HttpHeader.Null) { 854 bufferInFillMode.put(header.getBytesColonSpace()); 855 putSanitisedValue(field.getValue(), bufferInFillMode); 856 } else { 857 putSanitisedName(field.getName(), bufferInFillMode); 858 bufferInFillMode.put(__colon_space); 859 putSanitisedValue(field.getValue(), bufferInFillMode); 860 } 861 862 BufferUtils.putCRLF(bufferInFillMode); 863 } 864 } 865 866 static void putTo(HttpFields fields, ByteBuffer bufferInFillMode) { 867 foreach (HttpField field ; fields) { 868 if (field !is null) 869 putTo(field, bufferInFillMode); 870 } 871 BufferUtils.putCRLF(bufferInFillMode); 872 } 873 }