package io.craft.atom.protocol.http; import static io.craft.atom.protocol.http.HttpConstants.COLON; import static io.craft.atom.protocol.http.HttpConstants.CONTENT_ENCODING_DEFLATE; import static io.craft.atom.protocol.http.HttpConstants.CONTENT_ENCODING_GZIP; import static io.craft.atom.protocol.http.HttpConstants.CONTENT_ENCODING_IDENTITY; import static io.craft.atom.protocol.http.HttpConstants.CR; import static io.craft.atom.protocol.http.HttpConstants.EQUAL_SIGN; import static io.craft.atom.protocol.http.HttpConstants.HT; import static io.craft.atom.protocol.http.HttpConstants.LF; import static io.craft.atom.protocol.http.HttpConstants.NUL; import static io.craft.atom.protocol.http.HttpConstants.SEMICOLON; import static io.craft.atom.protocol.http.HttpConstants.SP; import static io.craft.atom.protocol.http.HttpConstants.TRANSFER_ENCODING_CHUNKED; import io.craft.atom.protocol.AbstractProtocolDecoder; import io.craft.atom.protocol.ProtocolException; import io.craft.atom.protocol.ProtocolExceptionType; import io.craft.atom.protocol.http.model.HttpChunk; import io.craft.atom.protocol.http.model.HttpChunkEntity; import io.craft.atom.protocol.http.model.HttpContentType; import io.craft.atom.protocol.http.model.HttpEntity; import io.craft.atom.protocol.http.model.HttpHeader; import io.craft.atom.protocol.http.model.HttpHeaderType; import io.craft.atom.protocol.http.model.HttpHeaderValueElement; import io.craft.atom.protocol.http.model.HttpMessage; import io.craft.atom.protocol.http.model.MimeType; import io.craft.atom.util.ByteUtil; import io.craft.atom.util.GzipUtil; import java.io.IOException; import java.nio.charset.Charset; import java.util.List; import lombok.Getter; import lombok.Setter; import lombok.ToString; /** * A http decoder for {@code HttpRequest} and {@code HttpResponse} * * @author mindwind * @version 1.0, Feb 3, 2013 * @param <T> * @see HttpRequestDecoder * @see HttpResponseDecoder */ @ToString(callSuper = true) abstract public class HttpDecoder<T extends HttpMessage> extends AbstractProtocolDecoder { protected static final int METHOD = 11; protected static final int REQUEST_URI = 12; protected static final int STATUS_CODE = 21; protected static final int REASON_PHRASE = 22; protected static final int VERSION = 30; protected static final int HEADER_NAME = 40; protected static final int HEADER_VALUE_PREFIX = 41; protected static final int HEADER_VALUE = 42; protected static final int HEADER_VALUE_SUFFIX = 43; protected static final int ENTITY = 50; protected static final int ENTITY_LENGTH = 51; protected static final int ENTITY_CHUNKED_SIZE = 52; protected static final int ENTITY_CHUNKED_EXTENSION_NAME = 53; protected static final int ENTITY_CHUNKED_EXTENSION_VALUE = 54; protected static final int ENTITY_CHUNKED_DATA = 55; protected static final int ENTITY_CHUNKED_TRAILER_NAME = 56; protected static final int ENTITY_CHUNKED_TRAILER_VALUE = 57; protected static final int ENTITY_ENCODING = 58; @Getter @Setter protected int maxLineLength = defaultBufferSize; @Getter protected int trailerSize ; @Getter protected HttpHeader header ; @Getter protected HttpEntity entity ; @Getter protected HttpChunk chunk ; @Getter protected HttpContentType contentType ; @Getter protected String chunkExtName ; @Getter protected T httpMessage ; // ~ ------------------------------------------------------------------------------------------------------------ @Override public void reset() { super.reset(); maxLineLength = defaultBufferSize; stateIndex = 0 ; state = START ; trailerSize = 0 ; header = null ; entity = null ; chunk = null ; contentType = null ; chunkExtName = null ; httpMessage = null ; } protected void state4END(List<T> httpMessages) throws ProtocolException { // enter END state means search index stay for the last byte of the HttpMessage, move to next slide(1); httpMessages.add(httpMessage); splitIndex = stateIndex = searchIndex; clear(); state = START; } protected void state4ENTITY_ENCODING() throws ProtocolException, IOException { HttpHeader ceh = httpMessage.getFirstHeader(HttpHeaderType.CONTENT_ENCODING.getName()); String coding = null; if (ceh == null){ coding = CONTENT_ENCODING_IDENTITY; } else { coding = ceh.getValue(); } // none or identity if (coding == null || CONTENT_ENCODING_IDENTITY.equals(coding)) { // if content is chunked, getContent() will restructure chunked content to a complete content. httpMessage.getEntity().setContent(httpMessage.getEntity().getContent()); } // gzip else if (CONTENT_ENCODING_GZIP.equals(coding)) { byte[] content = GzipUtil.ungzip(httpMessage.getEntity().getContent()); httpMessage.getEntity().setContent(content); } // deflate else if (CONTENT_ENCODING_DEFLATE.equals(coding)) { /* * A zlib stream will have a header. * * CMF | FLG [| DICTID ] | ...compressed data | ADLER32 | * * * CMF is one byte. * * * FLG is one byte. * * * DICTID is four bytes, and only present if FLG.FDICT is set. * * Sniff the content. Does it look like a zlib stream, with a CMF, etc? * c.f. RFC1950, section 2.2. http://tools.ietf.org/html/rfc1950#page-4 * * We need to see if it looks like a proper zlib stream, or whether it * is just a deflate stream. RFC2616 calls zlib streams deflate. * Confusing, isn't it? That's why some servers implement deflate * Content-Encoding using deflate streams, rather than zlib streams. * * We could start looking at the bytes, but to be honest, someone else * has already read the RFCs and implemented that for us. So we'll just * use the JDK libraries and exception handling to do this. If that * proves slow, then we could potentially change this to check the first * byte - does it look like a CMF? What about the second byte - does it * look like a FLG, etc. * * Many browsers over the years implemented an incorrect deflate algorithm, * For example: deflate works in Safari 4.0 but is broken in Safari 5.1, it also always has issues on IE. * So, we don't support deflate encoding now. */ throw new ProtocolException(ProtocolExceptionType.UNEXPECTED, "unsupported content encoding=" + coding); } // compress or others encoding is unsupported else { throw new ProtocolException(ProtocolExceptionType.UNEXPECTED, "unsupported content encoding=" + coding); } // next state httpMessage.setEntity(entity); state = END; } protected void state4ENTITY_CHUNKED_TRAILER_NAME() throws ProtocolException { boolean done = skip(CR, LF); if (!done) { return; } // slice header name String name = sliceBySeparators(0, COLON); if (name == null) { return; } header = new HttpHeader(); header.setName(name); // to next state trailerSize--; state = ENTITY_CHUNKED_TRAILER_VALUE; } protected void state4ENTITY_CHUNKED_TRAILER_VALUE() throws ProtocolException { // skip SP or HT boolean done = skip(SP, HT); if (!done) { return; } // slice header value String value = sliceBySeparators(-1, LF); if (value == null) { return; } header.appendValue(value); ((HttpChunkEntity) entity).addTrailer(header); // to next state if (trailerSize == 0) { slide(-1); state = END; } else { state = ENTITY_CHUNKED_TRAILER_NAME; } } protected void state4ENTITY_CHUNKED_DATA() throws ProtocolException { boolean done = skip(CR, LF); if (!done) { return; } // slice content value int clen = chunk.getSize(); byte[] chunkData = sliceByLength(clen); if (chunkData == null) { return; } chunk.setData(chunkData); ((HttpChunkEntity) entity).addChunk(chunk); // skip CRLF slide(2); state = ENTITY_CHUNKED_SIZE; } protected void state4ENTITY_CHUNKED_EXTENSION_VALUE() throws ProtocolException { // slice chunk extension value String chunkExtValue = sliceBySeparators(0, SEMICOLON, CR); if (chunkExtValue == null) { return; } chunk.addExtension(chunkExtName, chunkExtValue); byte pb = previousByte(); if (SEMICOLON == pb) { state = ENTITY_CHUNKED_EXTENSION_NAME; } else { state = ENTITY_CHUNKED_DATA; } } protected void state4ENTITY_CHUNKED_EXTENSION_NAME() throws ProtocolException { // slice chunk extension name String name = sliceBySeparators(0, EQUAL_SIGN, CR); if (name == null) { return; } this.chunkExtName = name; byte pb = previousByte(); if (EQUAL_SIGN == pb) { state = ENTITY_CHUNKED_EXTENSION_VALUE; } else { chunk.addExtension(chunkExtName, null); state = ENTITY_CHUNKED_DATA; } } protected void state4ENTITY_CHUNKED_SIZE() throws ProtocolException { boolean done = skip(LF); if (!done) { return; } // slice chunk size String sizeStr = sliceBySeparators(0, SEMICOLON, CR); if (sizeStr == null) { return; } int size = Integer.parseInt(sizeStr, 16); if (size < 0) { throw new ProtocolException(ProtocolExceptionType.UNEXPECTED, "chunked size < 0"); } chunk = new HttpChunk(); chunk.setSize(size); byte pb = previousByte(); if (SEMICOLON == pb) { state = ENTITY_CHUNKED_EXTENSION_NAME; } else if (size > 0){ state = ENTITY_CHUNKED_DATA; } else if (size == 0) { HttpHeader trailerHeader = httpMessage.getFirstHeader(HttpHeaderType.TRAILER.getName()); httpMessage.setEntity(entity); if (trailerHeader != null) { trailerSize = trailerHeader.getValue().split(",").length; if (trailerSize <= 0) { throw new ProtocolException(ProtocolExceptionType.UNEXPECTED, "trailer size ilegal=" + trailerSize); } state = ENTITY_CHUNKED_TRAILER_NAME; } else { state = ENTITY_ENCODING; } } } protected void state4ENTITY_LENGTH() throws ProtocolException { // get content length int clen = Integer.parseInt(httpMessage.getFirstHeader(HttpHeaderType.CONTENT_LENGTH.getName()).getValue()); if (clen < 0) { throw new ProtocolException(ProtocolExceptionType.UNEXPECTED, "content length < 0"); } // slice content value byte[] content = sliceByLength(clen); if (content == null) { return; } // render current request with entity entity.setContent(content); httpMessage.setEntity(entity); // to next state state = ENTITY_ENCODING; } protected void state4ENTITY() throws ProtocolException { boolean done = skip(CR, LF); if (!done) { return; } // content length if (httpMessage.getFirstHeader(HttpHeaderType.CONTENT_LENGTH.getName()) != null) { entity = new HttpEntity(); entity.setContentType(getContentType(httpMessage)); state = ENTITY_LENGTH; } // chunked else if (TRANSFER_ENCODING_CHUNKED.equals(httpMessage.getFirstHeader(HttpHeaderType.TRANSFER_ENCODING.getName()).getValue())) { entity = new HttpChunkEntity(); entity.setContentType(getContentType(httpMessage)); state = ENTITY_CHUNKED_SIZE; } // no entity else { state = END; } } protected void state4HEADER_VALUE_SUFFIX() throws ProtocolException { byte cb = currentByte(); // folded header if (SP == cb || HT == cb) { state = HEADER_VALUE_PREFIX; } // header end else if (CR == cb) { httpMessage.addHeader(header); state = hasEntity(httpMessage) ? ENTITY : END; slide(1); } // next header else { httpMessage.addHeader(header); state = HEADER_NAME; } } protected void state4HEADER_VALUE() throws ProtocolException { // slice header value String value = sliceBySeparators(-1, LF); if (value == null) { return; } header.appendValue(value); // to next state byte cb = currentByte(); // has no next byte in buffer if (NUL == cb) { state = HEADER_VALUE_SUFFIX; } // folded header else if (SP == cb || HT == cb) { state = HEADER_VALUE_PREFIX; } // header end else if (CR == cb) { httpMessage.addHeader(header); state = hasEntity(httpMessage) ? ENTITY : END; slide(1); } // next header else { httpMessage.addHeader(header); state = HEADER_NAME; } } protected void state4HEADER_VALUE_PREFIX() throws ProtocolException { // skip SP or HT boolean done = skip(SP, HT); if (!done) { return; } // to next state state = HEADER_VALUE; } protected void state4HEADER_NAME() throws ProtocolException { // slice header name String name = sliceBySeparators(0, COLON); if (name == null) { return; } header = new HttpHeader(); header.setName(name); // to next state state = HEADER_VALUE_PREFIX; } protected void adapt() { if (splitIndex > 0 && splitIndex < buf.length()) { byte[] tailBytes = buf.array(splitIndex, buf.length()); buf.clear(); buf.append(tailBytes); stateIndex -= splitIndex; searchIndex = buf.length(); splitIndex = 0; } if (splitIndex > 0 && splitIndex == buf.length()) { buf.clear(); stateIndex = splitIndex = searchIndex = 0; } if (buf.length() == 0 && buf.capacity() > maxSize * 2) { buf.reset(defaultBufferSize); } } protected HttpContentType getContentType(HttpMessage httpMessage) { if (contentType != null) { return contentType; } // No Content-Type header HttpHeader contentTypeHeader = httpMessage.getFirstHeader(HttpHeaderType.CONTENT_TYPE.getName()); if (contentTypeHeader == null) { contentType = new HttpContentType(charset); return contentType; } // value element is null, e.g. Content-Type: List<HttpHeaderValueElement> elements = contentTypeHeader.getValueElements(); if (elements.isEmpty()) { contentType = new HttpContentType(charset); return contentType; } // e.g. Content-Type: text/plain; charset=utf-8 HttpHeaderValueElement element = elements.get(0); Charset contentCharset = charset; String mimeType = element.getName(); String charsetName = element.getParamValue(HttpConstants.CHARSET); if (charsetName != null) { contentCharset = Charset.forName(charsetName); } contentType = new HttpContentType(MimeType.from(mimeType), contentCharset); return contentType; } protected byte previousByte() { int i = searchIndex - 1; if ( i < buf.length()) { return buf.byteAt(i); } else { return NUL; } } protected byte currentByte() { int i = searchIndex; if ( i < buf.length()) { return buf.byteAt(i); } else { return NUL; } } protected byte nextByte() { int i = searchIndex + 1; if (i < buf.length()) { return buf.byteAt(i); } else { return NUL; } } protected byte[] sliceByLength(int len) throws ProtocolException { boolean done = false; int offset = stateIndex; int length = searchIndex - stateIndex; if (len < length) { throw new ProtocolException(ProtocolExceptionType.UNEXPECTED, "slice len=" + len + "< length=" + length); } int tailIndex = offset + len - 1; if (tailIndex < buf.length()) { stateIndex = searchIndex = tailIndex; done = true; } else { searchIndex = buf.length(); } if (searchIndex > maxSize) { throw new ProtocolException(ProtocolExceptionType.MAX_SIZE_LIMIT, maxSize); } if (done) { byte[] bytes = new byte[len]; System.arraycopy(buf.buffer(), offset, bytes, 0, len); return bytes; } else { return null; } } protected String sliceBySeparators(int shift, byte... separators) throws ProtocolException { boolean done = false; int offset = stateIndex; int length = searchIndex - stateIndex; for (int i = searchIndex; i < buf.length(); length++) { if (length > maxLineLength) { throw new ProtocolException(ProtocolExceptionType.LINE_LENGTH_LIMIT, maxLineLength); } byte b = buf.byteAt(i); searchIndex = ++i; if (ByteUtil.indexOf(separators, b) >= 0) { length = searchIndex - stateIndex - 1; stateIndex = searchIndex; done = true; break; } } if (searchIndex > maxSize) { throw new ProtocolException(ProtocolExceptionType.MAX_SIZE_LIMIT, maxSize); } if (done) { return new String(buf.buffer(), offset, length + shift, charset); } else { return null; } } protected boolean skip(byte... bytes) throws ProtocolException { boolean done = false; int length = searchIndex - stateIndex; while (searchIndex < buf.length()) { byte cb = currentByte(); if (ByteUtil.indexOf(bytes, cb) < 0 && NUL != cb) { done = true; break; } slide(1); length++; if (length > maxSize) { throw new ProtocolException(ProtocolExceptionType.MAX_SIZE_LIMIT, maxSize); } } return done; } protected void slide(int shift) throws ProtocolException { stateIndex = searchIndex += shift; if (searchIndex > maxSize) { throw new ProtocolException(ProtocolExceptionType.MAX_SIZE_LIMIT, maxSize); } } protected void clear() { header = null; entity = null; chunk = null; chunkExtName = null; trailerSize = 0; httpMessage = null; } protected void resetIndex() { searchIndex = splitIndex = stateIndex = 0; } abstract boolean hasEntity(T httpMessage); }