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 }