1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.http;
20
21 import java.nio.ByteBuffer;
22 import java.nio.charset.StandardCharsets;
23 import java.util.Arrays;
24 import java.util.EnumSet;
25 import java.util.Locale;
26
27 import org.eclipse.jetty.http.HttpTokens.EndOfContent;
28 import org.eclipse.jetty.util.ArrayTernaryTrie;
29 import org.eclipse.jetty.util.ArrayTrie;
30 import org.eclipse.jetty.util.BufferUtil;
31 import org.eclipse.jetty.util.HostPort;
32 import org.eclipse.jetty.util.StringUtil;
33 import org.eclipse.jetty.util.Trie;
34 import org.eclipse.jetty.util.TypeUtil;
35 import org.eclipse.jetty.util.Utf8StringBuilder;
36 import org.eclipse.jetty.util.log.Log;
37 import org.eclipse.jetty.util.log.Logger;
38
39 import static org.eclipse.jetty.http.HttpCompliance.LEGACY;
40 import static org.eclipse.jetty.http.HttpCompliance.RFC2616;
41 import static org.eclipse.jetty.http.HttpCompliance.RFC7230;
42 import static org.eclipse.jetty.http.HttpTokens.CARRIAGE_RETURN;
43 import static org.eclipse.jetty.http.HttpTokens.LINE_FEED;
44 import static org.eclipse.jetty.http.HttpTokens.SPACE;
45 import static org.eclipse.jetty.http.HttpTokens.TAB;
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92 public class HttpParser
93 {
94 public static final Logger LOG = Log.getLogger(HttpParser.class);
95 @Deprecated
96 public final static String __STRICT="org.eclipse.jetty.http.HttpParser.STRICT";
97 public final static int INITIAL_URI_LENGTH=256;
98
99
100
101
102
103
104
105
106
107
108
109
110
111 public final static Trie<HttpField> CACHE = new ArrayTrie<>(2048);
112
113
114 public enum State
115 {
116 START,
117 METHOD,
118 RESPONSE_VERSION,
119 SPACE1,
120 STATUS,
121 URI,
122 SPACE2,
123 REQUEST_VERSION,
124 REASON,
125 PROXY,
126 HEADER,
127 HEADER_IN_NAME,
128 HEADER_VALUE,
129 HEADER_IN_VALUE,
130 CONTENT,
131 EOF_CONTENT,
132 CHUNKED_CONTENT,
133 CHUNK_SIZE,
134 CHUNK_PARAMS,
135 CHUNK,
136 CHUNK_END,
137 END,
138 CLOSE,
139 CLOSED
140 }
141
142 private final static EnumSet<State> __idleStates = EnumSet.of(State.START,State.END,State.CLOSE,State.CLOSED);
143 private final static EnumSet<State> __completeStates = EnumSet.of(State.END,State.CLOSE,State.CLOSED);
144
145 private final boolean DEBUG=LOG.isDebugEnabled();
146 private final HttpHandler _handler;
147 private final RequestHandler _requestHandler;
148 private final ResponseHandler _responseHandler;
149 private final ComplianceHandler _complianceHandler;
150 private final int _maxHeaderBytes;
151 private final HttpCompliance _compliance;
152 private HttpField _field;
153 private HttpHeader _header;
154 private String _headerString;
155 private HttpHeaderValue _value;
156 private String _valueString;
157 private int _responseStatus;
158 private int _headerBytes;
159 private boolean _host;
160
161
162 private volatile State _state=State.START;
163 private volatile boolean _eof;
164 private HttpMethod _method;
165 private String _methodString;
166 private HttpVersion _version;
167 private Utf8StringBuilder _uri=new Utf8StringBuilder(INITIAL_URI_LENGTH);
168 private EndOfContent _endOfContent;
169 private long _contentLength;
170 private long _contentPosition;
171 private int _chunkLength;
172 private int _chunkPosition;
173 private boolean _headResponse;
174 private boolean _cr;
175 private ByteBuffer _contentChunk;
176 private Trie<HttpField> _connectionFields;
177
178 private int _length;
179 private final StringBuilder _string=new StringBuilder();
180
181 static
182 {
183 CACHE.put(new HttpField(HttpHeader.CONNECTION,HttpHeaderValue.CLOSE));
184 CACHE.put(new HttpField(HttpHeader.CONNECTION,HttpHeaderValue.KEEP_ALIVE));
185 CACHE.put(new HttpField(HttpHeader.CONNECTION,HttpHeaderValue.UPGRADE));
186 CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING,"gzip"));
187 CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING,"gzip, deflate"));
188 CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING,"gzip,deflate,sdch"));
189 CACHE.put(new HttpField(HttpHeader.ACCEPT_LANGUAGE,"en-US,en;q=0.5"));
190 CACHE.put(new HttpField(HttpHeader.ACCEPT_LANGUAGE,"en-GB,en-US;q=0.8,en;q=0.6"));
191 CACHE.put(new HttpField(HttpHeader.ACCEPT_CHARSET,"ISO-8859-1,utf-8;q=0.7,*;q=0.3"));
192 CACHE.put(new HttpField(HttpHeader.ACCEPT,"*/*"));
193 CACHE.put(new HttpField(HttpHeader.ACCEPT,"image/png,image/*;q=0.8,*/*;q=0.5"));
194 CACHE.put(new HttpField(HttpHeader.ACCEPT,"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"));
195 CACHE.put(new HttpField(HttpHeader.PRAGMA,"no-cache"));
196 CACHE.put(new HttpField(HttpHeader.CACHE_CONTROL,"private, no-cache, no-cache=Set-Cookie, proxy-revalidate"));
197 CACHE.put(new HttpField(HttpHeader.CACHE_CONTROL,"no-cache"));
198 CACHE.put(new HttpField(HttpHeader.CONTENT_LENGTH,"0"));
199 CACHE.put(new HttpField(HttpHeader.CONTENT_ENCODING,"gzip"));
200 CACHE.put(new HttpField(HttpHeader.CONTENT_ENCODING,"deflate"));
201 CACHE.put(new HttpField(HttpHeader.TRANSFER_ENCODING,"chunked"));
202 CACHE.put(new HttpField(HttpHeader.EXPIRES,"Fri, 01 Jan 1990 00:00:00 GMT"));
203
204
205 for (String type : new String[]{"text/plain","text/html","text/xml","text/json","application/json","application/x-www-form-urlencoded"})
206 {
207 HttpField field=new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,type);
208 CACHE.put(field);
209
210 for (String charset : new String[]{"utf-8","iso-8859-1"})
211 {
212 CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,type+";charset="+charset));
213 CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,type+"; charset="+charset));
214 CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,type+";charset="+charset.toUpperCase(Locale.ENGLISH)));
215 CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,type+"; charset="+charset.toUpperCase(Locale.ENGLISH)));
216 }
217 }
218
219
220 for (HttpHeader h:HttpHeader.values())
221 if (!CACHE.put(new HttpField(h,(String)null)))
222 throw new IllegalStateException("CACHE FULL");
223
224 CACHE.put(new HttpField(HttpHeader.REFERER,(String)null));
225 CACHE.put(new HttpField(HttpHeader.IF_MODIFIED_SINCE,(String)null));
226 CACHE.put(new HttpField(HttpHeader.IF_NONE_MATCH,(String)null));
227 CACHE.put(new HttpField(HttpHeader.AUTHORIZATION,(String)null));
228 CACHE.put(new HttpField(HttpHeader.COOKIE,(String)null));
229 }
230
231 private static HttpCompliance compliance()
232 {
233 Boolean strict = Boolean.getBoolean(__STRICT);
234 return strict?HttpCompliance.LEGACY:HttpCompliance.RFC7230;
235 }
236
237
238 public HttpParser(RequestHandler handler)
239 {
240 this(handler,-1,compliance());
241 }
242
243
244 public HttpParser(ResponseHandler handler)
245 {
246 this(handler,-1,compliance());
247 }
248
249
250 public HttpParser(RequestHandler handler,int maxHeaderBytes)
251 {
252 this(handler,maxHeaderBytes,compliance());
253 }
254
255
256 public HttpParser(ResponseHandler handler,int maxHeaderBytes)
257 {
258 this(handler,maxHeaderBytes,compliance());
259 }
260
261
262 @Deprecated
263 public HttpParser(RequestHandler handler,int maxHeaderBytes,boolean strict)
264 {
265 this(handler,maxHeaderBytes,strict?HttpCompliance.LEGACY:compliance());
266 }
267
268
269 @Deprecated
270 public HttpParser(ResponseHandler handler,int maxHeaderBytes,boolean strict)
271 {
272 this(handler,maxHeaderBytes,strict?HttpCompliance.LEGACY:compliance());
273 }
274
275
276 public HttpParser(RequestHandler handler,HttpCompliance compliance)
277 {
278 this(handler,-1,compliance);
279 }
280
281
282 public HttpParser(RequestHandler handler,int maxHeaderBytes,HttpCompliance compliance)
283 {
284 _handler=handler;
285 _requestHandler=handler;
286 _responseHandler=null;
287 _maxHeaderBytes=maxHeaderBytes;
288 _compliance=compliance==null?compliance():compliance;
289 _complianceHandler=(ComplianceHandler)(handler instanceof ComplianceHandler?handler:null);
290 }
291
292
293 public HttpParser(ResponseHandler handler,int maxHeaderBytes,HttpCompliance compliance)
294 {
295 _handler=handler;
296 _requestHandler=null;
297 _responseHandler=handler;
298 _maxHeaderBytes=maxHeaderBytes;
299 _compliance=compliance==null?compliance():compliance;
300 _complianceHandler=(ComplianceHandler)(handler instanceof ComplianceHandler?handler:null);
301 }
302
303
304 public HttpHandler getHandler()
305 {
306 return _handler;
307 }
308
309
310
311
312
313
314
315 protected boolean complianceViolation(HttpCompliance compliance,String reason)
316 {
317 if (_complianceHandler==null)
318 return _compliance.ordinal()>=compliance.ordinal();
319 if (_compliance.ordinal()<compliance.ordinal())
320 {
321 _complianceHandler.onComplianceViolation(_compliance,compliance,reason);
322 return false;
323 }
324 return true;
325 }
326
327
328 protected String legacyString(String orig, String cached)
329 {
330 return (_compliance!=LEGACY || orig.equals(cached) || complianceViolation(RFC2616,"case sensitive"))?cached:orig;
331 }
332
333
334 public long getContentLength()
335 {
336 return _contentLength;
337 }
338
339
340 public long getContentRead()
341 {
342 return _contentPosition;
343 }
344
345
346
347
348
349 public void setHeadResponse(boolean head)
350 {
351 _headResponse=head;
352 }
353
354
355 protected void setResponseStatus(int status)
356 {
357 _responseStatus=status;
358 }
359
360
361 public State getState()
362 {
363 return _state;
364 }
365
366
367 public boolean inContentState()
368 {
369 return _state.ordinal()>=State.CONTENT.ordinal() && _state.ordinal()<State.END.ordinal();
370 }
371
372
373 public boolean inHeaderState()
374 {
375 return _state.ordinal() < State.CONTENT.ordinal();
376 }
377
378
379 public boolean isChunking()
380 {
381 return _endOfContent==EndOfContent.CHUNKED_CONTENT;
382 }
383
384
385 public boolean isStart()
386 {
387 return isState(State.START);
388 }
389
390
391 public boolean isClose()
392 {
393 return isState(State.CLOSE);
394 }
395
396
397 public boolean isClosed()
398 {
399 return isState(State.CLOSED);
400 }
401
402
403 public boolean isIdle()
404 {
405 return __idleStates.contains(_state);
406 }
407
408
409 public boolean isComplete()
410 {
411 return __completeStates.contains(_state);
412 }
413
414
415 public boolean isState(State state)
416 {
417 return _state == state;
418 }
419
420
421 enum CharState { ILLEGAL, CR, LF, LEGAL }
422 private final static CharState[] __charState;
423 static
424 {
425
426
427
428
429
430
431
432
433
434
435
436
437 __charState=new CharState[256];
438 Arrays.fill(__charState,CharState.ILLEGAL);
439 __charState[LINE_FEED]=CharState.LF;
440 __charState[CARRIAGE_RETURN]=CharState.CR;
441 __charState[TAB]=CharState.LEGAL;
442 __charState[SPACE]=CharState.LEGAL;
443
444 __charState['!']=CharState.LEGAL;
445 __charState['#']=CharState.LEGAL;
446 __charState['$']=CharState.LEGAL;
447 __charState['%']=CharState.LEGAL;
448 __charState['&']=CharState.LEGAL;
449 __charState['\'']=CharState.LEGAL;
450 __charState['*']=CharState.LEGAL;
451 __charState['+']=CharState.LEGAL;
452 __charState['-']=CharState.LEGAL;
453 __charState['.']=CharState.LEGAL;
454 __charState['^']=CharState.LEGAL;
455 __charState['_']=CharState.LEGAL;
456 __charState['`']=CharState.LEGAL;
457 __charState['|']=CharState.LEGAL;
458 __charState['~']=CharState.LEGAL;
459
460 __charState['"']=CharState.LEGAL;
461
462 __charState['\\']=CharState.LEGAL;
463 __charState['(']=CharState.LEGAL;
464 __charState[')']=CharState.LEGAL;
465 Arrays.fill(__charState,0x21,0x27+1,CharState.LEGAL);
466 Arrays.fill(__charState,0x2A,0x5B+1,CharState.LEGAL);
467 Arrays.fill(__charState,0x5D,0x7E+1,CharState.LEGAL);
468 Arrays.fill(__charState,0x80,0xFF+1,CharState.LEGAL);
469
470 }
471
472
473 private byte next(ByteBuffer buffer)
474 {
475 byte ch = buffer.get();
476
477 CharState s = __charState[0xff & ch];
478 switch(s)
479 {
480 case ILLEGAL:
481 throw new IllegalCharacterException(_state,ch,buffer);
482
483 case LF:
484 _cr=false;
485 break;
486
487 case CR:
488 if (_cr)
489 throw new BadMessageException("Bad EOL");
490
491 _cr=true;
492 if (buffer.hasRemaining())
493 {
494 if(_maxHeaderBytes>0 && _state.ordinal()<State.END.ordinal())
495 _headerBytes++;
496 return next(buffer);
497 }
498
499
500
501 return 0;
502
503 case LEGAL:
504 if (_cr)
505 throw new BadMessageException("Bad EOL");
506
507 }
508
509 return ch;
510 }
511
512
513
514
515
516 private boolean quickStart(ByteBuffer buffer)
517 {
518 if (_requestHandler!=null)
519 {
520 _method = HttpMethod.lookAheadGet(buffer);
521 if (_method!=null)
522 {
523 _methodString = _method.asString();
524 buffer.position(buffer.position()+_methodString.length()+1);
525
526 setState(State.SPACE1);
527 return false;
528 }
529 }
530 else if (_responseHandler!=null)
531 {
532 _version = HttpVersion.lookAheadGet(buffer);
533 if (_version!=null)
534 {
535 buffer.position(buffer.position()+_version.asString().length()+1);
536 setState(State.SPACE1);
537 return false;
538 }
539 }
540
541
542 while (_state==State.START && buffer.hasRemaining())
543 {
544 int ch=next(buffer);
545
546 if (ch > SPACE)
547 {
548 _string.setLength(0);
549 _string.append((char)ch);
550 setState(_requestHandler!=null?State.METHOD:State.RESPONSE_VERSION);
551 return false;
552 }
553 else if (ch==0)
554 break;
555 else if (ch<0)
556 throw new BadMessageException();
557
558
559 if (_maxHeaderBytes>0 && ++_headerBytes>_maxHeaderBytes)
560 {
561 LOG.warn("padding is too large >"+_maxHeaderBytes);
562 throw new BadMessageException(HttpStatus.BAD_REQUEST_400);
563 }
564 }
565 return false;
566 }
567
568
569 private void setString(String s)
570 {
571 _string.setLength(0);
572 _string.append(s);
573 _length=s.length();
574 }
575
576
577 private String takeString()
578 {
579 _string.setLength(_length);
580 String s =_string.toString();
581 _string.setLength(0);
582 _length=-1;
583 return s;
584 }
585
586
587
588
589 private boolean parseLine(ByteBuffer buffer)
590 {
591 boolean handle=false;
592
593
594 while (_state.ordinal()<State.HEADER.ordinal() && buffer.hasRemaining() && !handle)
595 {
596
597 byte ch=next(buffer);
598 if (ch==0)
599 break;
600
601 if (_maxHeaderBytes>0 && ++_headerBytes>_maxHeaderBytes)
602 {
603 if (_state==State.URI)
604 {
605 LOG.warn("URI is too large >"+_maxHeaderBytes);
606 throw new BadMessageException(HttpStatus.URI_TOO_LONG_414);
607 }
608 else
609 {
610 if (_requestHandler!=null)
611 LOG.warn("request is too large >"+_maxHeaderBytes);
612 else
613 LOG.warn("response is too large >"+_maxHeaderBytes);
614 throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431);
615 }
616 }
617
618 switch (_state)
619 {
620 case METHOD:
621 if (ch == SPACE)
622 {
623 _length=_string.length();
624 _methodString=takeString();
625 HttpMethod method=HttpMethod.CACHE.get(_methodString);
626 if (method!=null)
627 _methodString=legacyString(_methodString,method.asString());
628 setState(State.SPACE1);
629 }
630 else if (ch < SPACE)
631 {
632 if (ch==LINE_FEED)
633 throw new BadMessageException("No URI");
634 else
635 throw new IllegalCharacterException(_state,ch,buffer);
636 }
637 else
638 _string.append((char)ch);
639 break;
640
641 case RESPONSE_VERSION:
642 if (ch == HttpTokens.SPACE)
643 {
644 _length=_string.length();
645 String version=takeString();
646 _version=HttpVersion.CACHE.get(version);
647 if (_version==null)
648 throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Unknown Version");
649 setState(State.SPACE1);
650 }
651 else if (ch < HttpTokens.SPACE)
652 throw new IllegalCharacterException(_state,ch,buffer);
653 else
654 _string.append((char)ch);
655 break;
656
657 case SPACE1:
658 if (ch > HttpTokens.SPACE || ch<0)
659 {
660 if (_responseHandler!=null)
661 {
662 setState(State.STATUS);
663 setResponseStatus(ch-'0');
664 }
665 else
666 {
667 _uri.reset();
668 setState(State.URI);
669
670 if (buffer.hasArray())
671 {
672 byte[] array=buffer.array();
673 int p=buffer.arrayOffset()+buffer.position();
674 int l=buffer.arrayOffset()+buffer.limit();
675 int i=p;
676 while (i<l && array[i]>HttpTokens.SPACE)
677 i++;
678
679 int len=i-p;
680 _headerBytes+=len;
681
682 if (_maxHeaderBytes>0 && ++_headerBytes>_maxHeaderBytes)
683 {
684 LOG.warn("URI is too large >"+_maxHeaderBytes);
685 throw new BadMessageException(HttpStatus.REQUEST_URI_TOO_LONG_414);
686 }
687 _uri.append(array,p-1,len+1);
688 buffer.position(i-buffer.arrayOffset());
689 }
690 else
691 _uri.append(ch);
692 }
693 }
694 else if (ch < HttpTokens.SPACE)
695 {
696 throw new BadMessageException(HttpStatus.BAD_REQUEST_400,_requestHandler!=null?"No URI":"No Status");
697 }
698 break;
699
700 case STATUS:
701 if (ch == HttpTokens.SPACE)
702 {
703 setState(State.SPACE2);
704 }
705 else if (ch>='0' && ch<='9')
706 {
707 _responseStatus=_responseStatus*10+(ch-'0');
708 }
709 else if (ch < HttpTokens.SPACE && ch>=0)
710 {
711 setState(State.HEADER);
712 handle=_responseHandler.startResponse(_version, _responseStatus, null)||handle;
713 }
714 else
715 {
716 throw new BadMessageException();
717 }
718 break;
719
720 case URI:
721 if (ch == HttpTokens.SPACE)
722 {
723 setState(State.SPACE2);
724 }
725 else if (ch < HttpTokens.SPACE && ch>=0)
726 {
727
728 if (complianceViolation(RFC7230,"HTTP/0.9"))
729 throw new BadMessageException("HTTP/0.9 not supported");
730 handle=_requestHandler.startRequest(_methodString,_uri.toString(), HttpVersion.HTTP_0_9);
731 setState(State.END);
732 BufferUtil.clear(buffer);
733 handle=_handler.headerComplete()||handle;
734 handle=_handler.messageComplete()||handle;
735 return handle;
736 }
737 else
738 {
739 _uri.append(ch);
740 }
741 break;
742
743 case SPACE2:
744 if (ch > HttpTokens.SPACE)
745 {
746 _string.setLength(0);
747 _string.append((char)ch);
748 if (_responseHandler!=null)
749 {
750 _length=1;
751 setState(State.REASON);
752 }
753 else
754 {
755 setState(State.REQUEST_VERSION);
756
757
758 HttpVersion version;
759 if (buffer.position()>0 && buffer.hasArray())
760 version=HttpVersion.lookAheadGet(buffer.array(),buffer.arrayOffset()+buffer.position()-1,buffer.arrayOffset()+buffer.limit());
761 else
762 version=HttpVersion.CACHE.getBest(buffer,0,buffer.remaining());
763
764 if (version!=null)
765 {
766 int pos = buffer.position()+version.asString().length()-1;
767 if (pos<buffer.limit())
768 {
769 byte n=buffer.get(pos);
770 if (n==HttpTokens.CARRIAGE_RETURN)
771 {
772 _cr=true;
773 _version=version;
774 _string.setLength(0);
775 buffer.position(pos+1);
776 }
777 else if (n==HttpTokens.LINE_FEED)
778 {
779 _version=version;
780 _string.setLength(0);
781 buffer.position(pos);
782 }
783 }
784 }
785 }
786 }
787 else if (ch == HttpTokens.LINE_FEED)
788 {
789 if (_responseHandler!=null)
790 {
791 setState(State.HEADER);
792 handle=_responseHandler.startResponse(_version, _responseStatus, null)||handle;
793 }
794 else
795 {
796
797 if (complianceViolation(RFC7230,"HTTP/0.9"))
798 throw new BadMessageException("HTTP/0.9 not supported");
799
800 handle=_requestHandler.startRequest(_methodString,_uri.toString(), HttpVersion.HTTP_0_9);
801 setState(State.END);
802 BufferUtil.clear(buffer);
803 handle=_handler.headerComplete()||handle;
804 handle=_handler.messageComplete()||handle;
805 return handle;
806 }
807 }
808 else if (ch<0)
809 throw new BadMessageException();
810 break;
811
812 case REQUEST_VERSION:
813 if (ch == HttpTokens.LINE_FEED)
814 {
815 if (_version==null)
816 {
817 _length=_string.length();
818 _version=HttpVersion.CACHE.get(takeString());
819 }
820 if (_version==null)
821 throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Unknown Version");
822
823
824 if (_connectionFields==null && _version.getVersion()>=HttpVersion.HTTP_1_1.getVersion() && _handler.getHeaderCacheSize()>0)
825 {
826 int header_cache = _handler.getHeaderCacheSize();
827 _connectionFields=new ArrayTernaryTrie<>(header_cache);
828 }
829
830 setState(State.HEADER);
831
832 handle=_requestHandler.startRequest(_methodString,_uri.toString(), _version)||handle;
833 continue;
834 }
835 else if (ch>=HttpTokens.SPACE)
836 _string.append((char)ch);
837 else
838 throw new BadMessageException();
839
840 break;
841
842 case REASON:
843 if (ch == HttpTokens.LINE_FEED)
844 {
845 String reason=takeString();
846 setState(State.HEADER);
847 handle=_responseHandler.startResponse(_version, _responseStatus, reason)||handle;
848 continue;
849 }
850 else if (ch>=HttpTokens.SPACE)
851 {
852 _string.append((char)ch);
853 if (ch!=' '&&ch!='\t')
854 _length=_string.length();
855 }
856 else
857 throw new BadMessageException();
858 break;
859
860 default:
861 throw new IllegalStateException(_state.toString());
862
863 }
864 }
865
866 return handle;
867 }
868
869 private void parsedHeader()
870 {
871
872 if (_headerString!=null || _valueString!=null)
873 {
874
875 if (_header!=null)
876 {
877 boolean add_to_connection_trie=false;
878 switch (_header)
879 {
880 case CONTENT_LENGTH:
881 if (_endOfContent == EndOfContent.CONTENT_LENGTH)
882 {
883 throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Duplicate Content-Length");
884 }
885 else if (_endOfContent != EndOfContent.CHUNKED_CONTENT)
886 {
887 _contentLength=convertContentLength(_valueString);
888 if (_contentLength <= 0)
889 _endOfContent=EndOfContent.NO_CONTENT;
890 else
891 _endOfContent=EndOfContent.CONTENT_LENGTH;
892 }
893 break;
894
895 case TRANSFER_ENCODING:
896 if (_value==HttpHeaderValue.CHUNKED)
897 {
898 _endOfContent=EndOfContent.CHUNKED_CONTENT;
899 _contentLength=-1;
900 }
901 else
902 {
903 if (_valueString.endsWith(HttpHeaderValue.CHUNKED.toString()))
904 _endOfContent=EndOfContent.CHUNKED_CONTENT;
905 else if (_valueString.contains(HttpHeaderValue.CHUNKED.toString()))
906 throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Bad chunking");
907 }
908 break;
909
910 case HOST:
911 _host=true;
912 if (!(_field instanceof HostPortHttpField) && _valueString!=null && !_valueString.isEmpty())
913 {
914 _field=new HostPortHttpField(_header,legacyString(_headerString,_header.asString()),_valueString);
915 add_to_connection_trie=_connectionFields!=null;
916 }
917 break;
918
919 case CONNECTION:
920
921 if (_valueString!=null && _valueString.contains("close"))
922 _connectionFields=null;
923
924 break;
925
926 case AUTHORIZATION:
927 case ACCEPT:
928 case ACCEPT_CHARSET:
929 case ACCEPT_ENCODING:
930 case ACCEPT_LANGUAGE:
931 case COOKIE:
932 case CACHE_CONTROL:
933 case USER_AGENT:
934 add_to_connection_trie=_connectionFields!=null && _field==null;
935 break;
936
937 default: break;
938
939 }
940
941 if (add_to_connection_trie && !_connectionFields.isFull() && _header!=null && _valueString!=null)
942 {
943 if (_field==null)
944 _field=new HttpField(_header,legacyString(_headerString,_header.asString()),_valueString);
945 _connectionFields.put(_field);
946 }
947 }
948 _handler.parsedHeader(_field!=null?_field:new HttpField(_header,_headerString,_valueString));
949 }
950
951 _headerString=_valueString=null;
952 _header=null;
953 _value=null;
954 _field=null;
955 }
956
957 private long convertContentLength(String valueString)
958 {
959 try
960 {
961 return Long.parseLong(valueString);
962 }
963 catch(NumberFormatException e)
964 {
965 LOG.ignore(e);
966 throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Invalid Content-Length Value");
967 }
968 }
969
970
971
972
973
974 protected boolean parseHeaders(ByteBuffer buffer)
975 {
976 boolean handle=false;
977
978
979 while (_state.ordinal()<State.CONTENT.ordinal() && buffer.hasRemaining() && !handle)
980 {
981
982 byte ch=next(buffer);
983 if (ch==0)
984 break;
985
986 if (_maxHeaderBytes>0 && ++_headerBytes>_maxHeaderBytes)
987 {
988 LOG.warn("Header is too large >"+_maxHeaderBytes);
989 throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431);
990 }
991
992 switch (_state)
993 {
994 case HEADER:
995 switch(ch)
996 {
997 case HttpTokens.COLON:
998 case HttpTokens.SPACE:
999 case HttpTokens.TAB:
1000 {
1001 if (complianceViolation(RFC7230,"header folding"))
1002 throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Header Folding");
1003
1004
1005 if (_valueString==null)
1006 {
1007 _string.setLength(0);
1008 _length=0;
1009 }
1010 else
1011 {
1012 setString(_valueString);
1013 _string.append(' ');
1014 _length++;
1015 _valueString=null;
1016 }
1017 setState(State.HEADER_VALUE);
1018 break;
1019 }
1020
1021 case HttpTokens.LINE_FEED:
1022 {
1023
1024 parsedHeader();
1025
1026 _contentPosition=0;
1027
1028
1029
1030
1031 if (!_host && _version==HttpVersion.HTTP_1_1 && _requestHandler!=null)
1032 {
1033 throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"No Host");
1034 }
1035
1036
1037 if (_responseHandler !=null &&
1038 (_responseStatus == 304 ||
1039 _responseStatus == 204 ||
1040 _responseStatus < 200))
1041 _endOfContent=EndOfContent.NO_CONTENT;
1042
1043
1044 else if (_endOfContent == EndOfContent.UNKNOWN_CONTENT)
1045 {
1046 if (_responseStatus == 0
1047 || _responseStatus == 304
1048 || _responseStatus == 204
1049 || _responseStatus < 200)
1050 _endOfContent=EndOfContent.NO_CONTENT;
1051 else
1052 _endOfContent=EndOfContent.EOF_CONTENT;
1053 }
1054
1055
1056 switch (_endOfContent)
1057 {
1058 case EOF_CONTENT:
1059 setState(State.EOF_CONTENT);
1060 handle=_handler.headerComplete()||handle;
1061 return handle;
1062
1063 case CHUNKED_CONTENT:
1064 setState(State.CHUNKED_CONTENT);
1065 handle=_handler.headerComplete()||handle;
1066 return handle;
1067
1068 case NO_CONTENT:
1069 setState(State.END);
1070 handle=_handler.headerComplete()||handle;
1071 handle=_handler.messageComplete()||handle;
1072 return handle;
1073
1074 default:
1075 setState(State.CONTENT);
1076 handle=_handler.headerComplete()||handle;
1077 return handle;
1078 }
1079 }
1080
1081 default:
1082 {
1083
1084 if (ch<HttpTokens.SPACE)
1085 throw new BadMessageException();
1086
1087
1088 parsedHeader();
1089
1090
1091 if (buffer.hasRemaining())
1092 {
1093
1094 HttpField field=_connectionFields==null?null:_connectionFields.getBest(buffer,-1,buffer.remaining());
1095 if (field==null)
1096 field=CACHE.getBest(buffer,-1,buffer.remaining());
1097
1098 if (field!=null)
1099 {
1100 final String n;
1101 final String v;
1102
1103 if (_compliance==LEGACY)
1104 {
1105
1106 String fn=field.getName();
1107 n=legacyString(BufferUtil.toString(buffer,buffer.position()-1,fn.length(),StandardCharsets.US_ASCII),fn);
1108 String fv=field.getValue();
1109 if (fv==null)
1110 v=null;
1111 else
1112 {
1113 v=legacyString(BufferUtil.toString(buffer,buffer.position()+fn.length()+1,fv.length(),StandardCharsets.ISO_8859_1),fv);
1114 field=new HttpField(field.getHeader(),n,v);
1115 }
1116 }
1117 else
1118 {
1119 n=field.getName();
1120 v=field.getValue();
1121 }
1122
1123 _header=field.getHeader();
1124 _headerString=n;
1125
1126 if (v==null)
1127 {
1128
1129 setState(State.HEADER_VALUE);
1130 _string.setLength(0);
1131 _length=0;
1132 buffer.position(buffer.position()+n.length()+1);
1133 break;
1134 }
1135 else
1136 {
1137
1138 int pos=buffer.position()+n.length()+v.length()+1;
1139 byte b=buffer.get(pos);
1140
1141 if (b==HttpTokens.CARRIAGE_RETURN || b==HttpTokens.LINE_FEED)
1142 {
1143 _field=field;
1144 _valueString=v;
1145 setState(State.HEADER_IN_VALUE);
1146
1147 if (b==HttpTokens.CARRIAGE_RETURN)
1148 {
1149 _cr=true;
1150 buffer.position(pos+1);
1151 }
1152 else
1153 buffer.position(pos);
1154 break;
1155 }
1156 else
1157 {
1158 setState(State.HEADER_IN_VALUE);
1159 setString(v);
1160 buffer.position(pos);
1161 break;
1162 }
1163 }
1164 }
1165 }
1166
1167
1168 setState(State.HEADER_IN_NAME);
1169 _string.setLength(0);
1170 _string.append((char)ch);
1171 _length=1;
1172
1173 }
1174 }
1175 break;
1176
1177 case HEADER_IN_NAME:
1178 if (ch==HttpTokens.COLON)
1179 {
1180 if (_headerString==null)
1181 {
1182 _headerString=takeString();
1183 _header=HttpHeader.CACHE.get(_headerString);
1184 }
1185 _length=-1;
1186
1187 setState(State.HEADER_VALUE);
1188 break;
1189 }
1190
1191 if (ch>HttpTokens.SPACE)
1192 {
1193 if (_header!=null)
1194 {
1195 setString(_header.asString());
1196 _header=null;
1197 _headerString=null;
1198 }
1199
1200 _string.append((char)ch);
1201 if (ch>HttpTokens.SPACE)
1202 _length=_string.length();
1203 break;
1204 }
1205
1206 if (ch==HttpTokens.LINE_FEED && !complianceViolation(RFC7230,"name only header"))
1207 {
1208 if (_headerString==null)
1209 {
1210 _headerString=takeString();
1211 _header=HttpHeader.CACHE.get(_headerString);
1212 }
1213 _value=null;
1214 _string.setLength(0);
1215 _valueString="";
1216 _length=-1;
1217
1218 setState(State.HEADER);
1219 break;
1220 }
1221
1222 throw new IllegalCharacterException(_state,ch,buffer);
1223
1224 case HEADER_VALUE:
1225 if (ch>HttpTokens.SPACE || ch<0)
1226 {
1227 _string.append((char)(0xff&ch));
1228 _length=_string.length();
1229 setState(State.HEADER_IN_VALUE);
1230 break;
1231 }
1232
1233 if (ch==HttpTokens.SPACE || ch==HttpTokens.TAB)
1234 break;
1235
1236 if (ch==HttpTokens.LINE_FEED)
1237 {
1238 _value=null;
1239 _string.setLength(0);
1240 _valueString="";
1241 _length=-1;
1242
1243 setState(State.HEADER);
1244 break;
1245 }
1246 throw new IllegalCharacterException(_state,ch,buffer);
1247
1248 case HEADER_IN_VALUE:
1249 if (ch>=HttpTokens.SPACE || ch<0 || ch==HttpTokens.TAB)
1250 {
1251 if (_valueString!=null)
1252 {
1253 setString(_valueString);
1254 _valueString=null;
1255 _field=null;
1256 }
1257 _string.append((char)(0xff&ch));
1258 if (ch>HttpTokens.SPACE || ch<0)
1259 _length=_string.length();
1260 break;
1261 }
1262
1263 if (ch==HttpTokens.LINE_FEED)
1264 {
1265 if (_length > 0)
1266 {
1267 _value=null;
1268 _valueString=takeString();
1269 _length=-1;
1270 }
1271 setState(State.HEADER);
1272 break;
1273 }
1274
1275 throw new IllegalCharacterException(_state,ch,buffer);
1276
1277 default:
1278 throw new IllegalStateException(_state.toString());
1279
1280 }
1281 }
1282
1283 return handle;
1284 }
1285
1286
1287
1288
1289
1290
1291
1292 public boolean parseNext(ByteBuffer buffer)
1293 {
1294 if (DEBUG)
1295 LOG.debug("parseNext s={} {}",_state,BufferUtil.toDetailString(buffer));
1296 try
1297 {
1298
1299 if (_state==State.START)
1300 {
1301 _version=null;
1302 _method=null;
1303 _methodString=null;
1304 _endOfContent=EndOfContent.UNKNOWN_CONTENT;
1305 _header=null;
1306 if (quickStart(buffer))
1307 return true;
1308 }
1309
1310
1311 if (_state.ordinal()>= State.START.ordinal() && _state.ordinal()<State.HEADER.ordinal())
1312 {
1313 if (parseLine(buffer))
1314 return true;
1315 }
1316
1317
1318 if (_state.ordinal()>= State.HEADER.ordinal() && _state.ordinal()<State.CONTENT.ordinal())
1319 {
1320 if (parseHeaders(buffer))
1321 return true;
1322 }
1323
1324
1325 if (_state.ordinal()>= State.CONTENT.ordinal() && _state.ordinal()<State.END.ordinal())
1326 {
1327
1328 if (_responseStatus>0 && _headResponse)
1329 {
1330 setState(State.END);
1331 return _handler.messageComplete();
1332 }
1333 else
1334 {
1335 if (parseContent(buffer))
1336 return true;
1337 }
1338 }
1339
1340
1341 if (_state==State.END)
1342 {
1343
1344 while (buffer.remaining()>0 && buffer.get(buffer.position())<=HttpTokens.SPACE)
1345 buffer.get();
1346 }
1347 else if (_state==State.CLOSE)
1348 {
1349
1350 if (BufferUtil.hasContent(buffer))
1351 {
1352
1353 _headerBytes+=buffer.remaining();
1354 BufferUtil.clear(buffer);
1355 if (_maxHeaderBytes>0 && _headerBytes>_maxHeaderBytes)
1356 {
1357
1358 throw new IllegalStateException("too much data seeking EOF");
1359 }
1360 }
1361 }
1362 else if (_state==State.CLOSED)
1363 {
1364 BufferUtil.clear(buffer);
1365 }
1366
1367
1368 if (_eof && !buffer.hasRemaining())
1369 {
1370 switch(_state)
1371 {
1372 case CLOSED:
1373 break;
1374
1375 case START:
1376 setState(State.CLOSED);
1377 _handler.earlyEOF();
1378 break;
1379
1380 case END:
1381 case CLOSE:
1382 setState(State.CLOSED);
1383 break;
1384
1385 case EOF_CONTENT:
1386 setState(State.CLOSED);
1387 return _handler.messageComplete();
1388
1389 case CONTENT:
1390 case CHUNKED_CONTENT:
1391 case CHUNK_SIZE:
1392 case CHUNK_PARAMS:
1393 case CHUNK:
1394 setState(State.CLOSED);
1395 _handler.earlyEOF();
1396 break;
1397
1398 default:
1399 if (DEBUG)
1400 LOG.debug("{} EOF in {}",this,_state);
1401 setState(State.CLOSED);
1402 _handler.badMessage(400,null);
1403 break;
1404 }
1405 }
1406 }
1407 catch(BadMessageException e)
1408 {
1409 BufferUtil.clear(buffer);
1410
1411 Throwable cause = e.getCause();
1412 boolean stack = LOG.isDebugEnabled() ||
1413 (!(cause instanceof NumberFormatException ) && (cause instanceof RuntimeException || cause instanceof Error));
1414
1415 if (stack)
1416 LOG.warn("bad HTTP parsed: "+e._code+(e.getReason()!=null?" "+e.getReason():"")+" for "+_handler,e);
1417 else
1418 LOG.warn("bad HTTP parsed: "+e._code+(e.getReason()!=null?" "+e.getReason():"")+" for "+_handler);
1419 setState(State.CLOSE);
1420 _handler.badMessage(e.getCode(), e.getReason());
1421 }
1422 catch(NumberFormatException|IllegalStateException e)
1423 {
1424 BufferUtil.clear(buffer);
1425 LOG.warn("parse exception: {} in {} for {}",e.toString(),_state,_handler);
1426 if (DEBUG)
1427 LOG.debug(e);
1428
1429 switch(_state)
1430 {
1431 case CLOSED:
1432 break;
1433 case CLOSE:
1434 _handler.earlyEOF();
1435 break;
1436 default:
1437 setState(State.CLOSE);
1438 _handler.badMessage(400,"Bad Message "+e.toString());
1439 }
1440 }
1441 catch(Exception|Error e)
1442 {
1443 BufferUtil.clear(buffer);
1444
1445 LOG.warn("parse exception: "+e.toString()+" for "+_handler,e);
1446
1447 switch(_state)
1448 {
1449 case CLOSED:
1450 break;
1451 case CLOSE:
1452 _handler.earlyEOF();
1453 break;
1454 default:
1455 setState(State.CLOSE);
1456 _handler.badMessage(400,null);
1457 }
1458 }
1459 return false;
1460 }
1461
1462 protected boolean parseContent(ByteBuffer buffer)
1463 {
1464 int remaining=buffer.remaining();
1465 if (remaining==0 && _state==State.CONTENT)
1466 {
1467 long content=_contentLength - _contentPosition;
1468 if (content == 0)
1469 {
1470 setState(State.END);
1471 return _handler.messageComplete();
1472 }
1473 }
1474
1475
1476 byte ch;
1477 while (_state.ordinal() < State.END.ordinal() && remaining>0)
1478 {
1479 switch (_state)
1480 {
1481 case EOF_CONTENT:
1482 _contentChunk=buffer.asReadOnlyBuffer();
1483 _contentPosition += remaining;
1484 buffer.position(buffer.position()+remaining);
1485 if (_handler.content(_contentChunk))
1486 return true;
1487 break;
1488
1489 case CONTENT:
1490 {
1491 long content=_contentLength - _contentPosition;
1492 if (content == 0)
1493 {
1494 setState(State.END);
1495 return _handler.messageComplete();
1496 }
1497 else
1498 {
1499 _contentChunk=buffer.asReadOnlyBuffer();
1500
1501
1502 if (remaining > content)
1503 {
1504
1505
1506 _contentChunk.limit(_contentChunk.position()+(int)content);
1507 }
1508
1509 _contentPosition += _contentChunk.remaining();
1510 buffer.position(buffer.position()+_contentChunk.remaining());
1511
1512 if (_handler.content(_contentChunk))
1513 return true;
1514
1515 if(_contentPosition == _contentLength)
1516 {
1517 setState(State.END);
1518 return _handler.messageComplete();
1519 }
1520 }
1521 break;
1522 }
1523
1524 case CHUNKED_CONTENT:
1525 {
1526 ch=next(buffer);
1527 if (ch>HttpTokens.SPACE)
1528 {
1529 _chunkLength=TypeUtil.convertHexDigit(ch);
1530 _chunkPosition=0;
1531 setState(State.CHUNK_SIZE);
1532 }
1533
1534 break;
1535 }
1536
1537 case CHUNK_SIZE:
1538 {
1539 ch=next(buffer);
1540 if (ch==0)
1541 break;
1542 if (ch == HttpTokens.LINE_FEED)
1543 {
1544 if (_chunkLength == 0)
1545 setState(State.CHUNK_END);
1546 else
1547 setState(State.CHUNK);
1548 }
1549 else if (ch <= HttpTokens.SPACE || ch == HttpTokens.SEMI_COLON)
1550 setState(State.CHUNK_PARAMS);
1551 else
1552 _chunkLength=_chunkLength * 16 + TypeUtil.convertHexDigit(ch);
1553 break;
1554 }
1555
1556 case CHUNK_PARAMS:
1557 {
1558 ch=next(buffer);
1559 if (ch == HttpTokens.LINE_FEED)
1560 {
1561 if (_chunkLength == 0)
1562 setState(State.CHUNK_END);
1563 else
1564 setState(State.CHUNK);
1565 }
1566 break;
1567 }
1568
1569 case CHUNK:
1570 {
1571 int chunk=_chunkLength - _chunkPosition;
1572 if (chunk == 0)
1573 {
1574 setState(State.CHUNKED_CONTENT);
1575 }
1576 else
1577 {
1578 _contentChunk=buffer.asReadOnlyBuffer();
1579
1580 if (remaining > chunk)
1581 _contentChunk.limit(_contentChunk.position()+chunk);
1582 chunk=_contentChunk.remaining();
1583
1584 _contentPosition += chunk;
1585 _chunkPosition += chunk;
1586 buffer.position(buffer.position()+chunk);
1587 if (_handler.content(_contentChunk))
1588 return true;
1589 }
1590 break;
1591 }
1592
1593 case CHUNK_END:
1594 {
1595
1596 ch=next(buffer);
1597 if (ch==0)
1598 break;
1599 if (ch == HttpTokens.LINE_FEED)
1600 {
1601 setState(State.END);
1602 return _handler.messageComplete();
1603 }
1604 throw new IllegalCharacterException(_state,ch,buffer);
1605 }
1606
1607 case CLOSED:
1608 {
1609 BufferUtil.clear(buffer);
1610 return false;
1611 }
1612
1613 default:
1614 break;
1615
1616 }
1617
1618 remaining=buffer.remaining();
1619 }
1620 return false;
1621 }
1622
1623
1624 public boolean isAtEOF()
1625
1626 {
1627 return _eof;
1628 }
1629
1630
1631
1632
1633 public void atEOF()
1634 {
1635 if (DEBUG)
1636 LOG.debug("atEOF {}", this);
1637 _eof=true;
1638 }
1639
1640
1641
1642
1643 public void close()
1644 {
1645 if (DEBUG)
1646 LOG.debug("close {}", this);
1647 setState(State.CLOSE);
1648 }
1649
1650
1651 public void reset()
1652 {
1653 if (DEBUG)
1654 LOG.debug("reset {}", this);
1655
1656
1657 if (_state==State.CLOSE || _state==State.CLOSED)
1658 return;
1659
1660 setState(State.START);
1661 _endOfContent=EndOfContent.UNKNOWN_CONTENT;
1662 _contentLength=-1;
1663 _contentPosition=0;
1664 _responseStatus=0;
1665 _contentChunk=null;
1666 _headerBytes=0;
1667 _host=false;
1668 }
1669
1670
1671 protected void setState(State state)
1672 {
1673 if (DEBUG)
1674 LOG.debug("{} --> {}",_state,state);
1675 _state=state;
1676 }
1677
1678
1679 public Trie<HttpField> getFieldCache()
1680 {
1681 return _connectionFields;
1682 }
1683
1684
1685 private String getProxyField(ByteBuffer buffer)
1686 {
1687 _string.setLength(0);
1688 _length=0;
1689
1690 while (buffer.hasRemaining())
1691 {
1692
1693 byte ch=next(buffer);
1694 if (ch<=' ')
1695 return _string.toString();
1696 _string.append((char)ch);
1697 }
1698 throw new BadMessageException();
1699 }
1700
1701
1702 @Override
1703 public String toString()
1704 {
1705 return String.format("%s{s=%s,%d of %d}",
1706 getClass().getSimpleName(),
1707 _state,
1708 _contentPosition,
1709 _contentLength);
1710 }
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722 public interface HttpHandler
1723 {
1724 public boolean content(ByteBuffer item);
1725
1726 public boolean headerComplete();
1727
1728 public boolean messageComplete();
1729
1730
1731
1732
1733
1734 public void parsedHeader(HttpField field);
1735
1736
1737
1738
1739
1740 public void earlyEOF();
1741
1742
1743
1744
1745
1746
1747 public void badMessage(int status, String reason);
1748
1749
1750
1751
1752 public int getHeaderCacheSize();
1753 }
1754
1755
1756
1757
1758 public interface RequestHandler extends HttpHandler
1759 {
1760
1761
1762
1763
1764
1765
1766
1767 public boolean startRequest(String method, String uri, HttpVersion version);
1768
1769 }
1770
1771
1772
1773
1774 public interface ResponseHandler extends HttpHandler
1775 {
1776
1777
1778
1779
1780
1781
1782
1783 public boolean startResponse(HttpVersion version, int status, String reason);
1784 }
1785
1786
1787
1788
1789 public interface ComplianceHandler extends HttpHandler
1790 {
1791 public void onComplianceViolation(HttpCompliance compliance,HttpCompliance required,String reason);
1792 }
1793
1794
1795 @SuppressWarnings("serial")
1796 private static class IllegalCharacterException extends BadMessageException
1797 {
1798 private IllegalCharacterException(State state,byte ch,ByteBuffer buffer)
1799 {
1800 super(400,String.format("Illegal character 0x%X",ch));
1801
1802 LOG.warn(String.format("Illegal character 0x%X in state=%s for buffer %s",ch,state,BufferUtil.toDetailString(buffer)));
1803 }
1804 }
1805 }