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