All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.firefly.codec.http2.encode.HttpGenerator Maven / Gradle / Ivy

There is a newer version: 4.0.3.2
Show newest version
package com.firefly.codec.http2.encode;

import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import com.firefly.codec.http2.model.BadMessageException;
import com.firefly.codec.http2.model.HttpField;
import com.firefly.codec.http2.model.HttpFields;
import com.firefly.codec.http2.model.HttpHeader;
import com.firefly.codec.http2.model.HttpHeaderValue;
import com.firefly.codec.http2.model.HttpMethod;
import com.firefly.codec.http2.model.HttpStatus;
import com.firefly.codec.http2.model.HttpTokens;
import com.firefly.codec.http2.model.HttpTokens.EndOfContent;
import com.firefly.codec.http2.model.HttpVersion;
import com.firefly.codec.http2.model.MetaData;
import com.firefly.codec.http2.model.PreEncodedHttpField;
import com.firefly.utils.StringUtils;
import com.firefly.utils.io.BufferUtils;
import com.firefly.utils.log.Log;
import com.firefly.utils.log.LogFactory;

/**
 * HttpGenerator. Builds HTTP Messages.
 * 

* If the system property "org.fireflysource.http.HttpGenerator.STRICT" is set * to true, then the generator will strictly pass on the exact strings received * from methods and header fields. Otherwise a fast case insensitive string * lookup is used that may alter the case and white space of some * methods/headers */ public class HttpGenerator { private static Log log = LogFactory.getInstance().getLog("firefly-system"); private final static byte[] __colon_space = new byte[] { ':', ' ' }; private final static HttpHeaderValue[] CLOSE = { HttpHeaderValue.CLOSE }; public static final MetaData.Response CONTINUE_100_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, 100, null, null, -1); public static final MetaData.Response PROGRESS_102_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, 102, null, null, -1); public final static MetaData.Response RESPONSE_500_INFO = new MetaData.Response(HttpVersion.HTTP_1_1, HttpStatus.INTERNAL_SERVER_ERROR_500, null, new HttpFields() {{ put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE); }}, 0); // states public enum State { START, COMMITTED, COMPLETING, COMPLETING_1XX, END } public enum Result { NEED_CHUNK, NEED_INFO, NEED_HEADER, FLUSH, CONTINUE, SHUTDOWN_OUT, DONE } // other statics public static final int CHUNK_SIZE = 12; private State _state = State.START; private EndOfContent _endOfContent = EndOfContent.UNKNOWN_CONTENT; private long _contentPrepared = 0; private boolean _noContent = false; private Boolean _persistent = null; private final int _send; private final static int SEND_SERVER = 0x01; private final static int SEND_XPOWEREDBY = 0x02; private final static Set __assumedContentMethods = new HashSet<>( Arrays.asList(new String[] { HttpMethod.POST.asString(), HttpMethod.PUT.asString() })); public static void setFireflyVersion(String serverVersion) { SEND[SEND_SERVER] = StringUtils.getBytes("Server: " + serverVersion + "\015\012"); SEND[SEND_XPOWEREDBY] = StringUtils.getBytes("X-Powered-By: " + serverVersion + "\015\012"); SEND[SEND_SERVER | SEND_XPOWEREDBY] = StringUtils .getBytes("Server: " + serverVersion + "\015\012X-Powered-By: " + serverVersion + "\015\012"); } // data private boolean _needCRLF = false; public HttpGenerator() { this(false, false); } public HttpGenerator(boolean sendServerVersion, boolean sendXPoweredBy) { _send = (sendServerVersion ? SEND_SERVER : 0) | (sendXPoweredBy ? SEND_XPOWEREDBY : 0); } public void reset() { _state = State.START; _endOfContent = EndOfContent.UNKNOWN_CONTENT; _noContent = false; _persistent = null; _contentPrepared = 0; _needCRLF = false; } public State getState() { return _state; } public boolean isState(State state) { return _state == state; } public boolean isIdle() { return _state == State.START; } public boolean isEnd() { return _state == State.END; } public boolean isCommitted() { return _state.ordinal() >= State.COMMITTED.ordinal(); } public boolean isChunking() { return _endOfContent == EndOfContent.CHUNKED_CONTENT; } public boolean isNoContent() { return _noContent; } public void setPersistent(boolean persistent) { _persistent = persistent; } /** * @return true if known to be persistent */ public boolean isPersistent() { return Boolean.TRUE.equals(_persistent); } public boolean isWritten() { return _contentPrepared > 0; } public long getContentPrepared() { return _contentPrepared; } public void abort() { _persistent = false; _state = State.END; _endOfContent = null; } public Result generateRequest(MetaData.Request info, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) { switch (_state) { case START: { if (info == null) return Result.NEED_INFO; if (header == null) return Result.NEED_HEADER; // If we have not been told our persistence, set the default if (_persistent == null) { _persistent = info.getVersion().ordinal() > HttpVersion.HTTP_1_0.ordinal(); if (!_persistent && HttpMethod.CONNECT.is(info.getMethod())) _persistent = true; } // prepare the header int pos = BufferUtils.flipToFill(header); try { // generate ResponseLine generateRequestLine(info, header); if (info.getVersion() == HttpVersion.HTTP_0_9) throw new BadMessageException(500, "HTTP/0.9 not supported"); generateHeaders(info, header, content, last); boolean expect100 = info.getFields().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()); if (expect100) { _state = State.COMMITTED; } else { // handle the content. int len = BufferUtils.length(content); if (len > 0) { _contentPrepared += len; if (isChunking()) prepareChunk(header, len); } _state = last ? State.COMPLETING : State.COMMITTED; } return Result.FLUSH; } catch (Exception e) { String message = (e instanceof BufferOverflowException) ? "Request header too large" : e.getMessage(); throw new BadMessageException(500, message, e); } finally { BufferUtils.flipToFlush(header, pos); } } case COMMITTED: { int len = BufferUtils.length(content); if (len > 0) { // Do we need a chunk buffer? if (isChunking()) { // Do we need a chunk buffer? if (chunk == null) return Result.NEED_CHUNK; BufferUtils.clearToFill(chunk); prepareChunk(chunk, len); BufferUtils.flipToFlush(chunk, 0); } _contentPrepared += len; } if (last) _state = State.COMPLETING; return len > 0 ? Result.FLUSH : Result.CONTINUE; } case COMPLETING: { if (BufferUtils.hasContent(content)) { if (log.isDebugEnabled()) log.debug("discarding content in COMPLETING"); BufferUtils.clear(content); } if (isChunking()) { // Do we need a chunk buffer? if (chunk == null) return Result.NEED_CHUNK; BufferUtils.clearToFill(chunk); prepareChunk(chunk, 0); BufferUtils.flipToFlush(chunk, 0); _endOfContent = EndOfContent.UNKNOWN_CONTENT; return Result.FLUSH; } _state = State.END; return Boolean.TRUE.equals(_persistent) ? Result.DONE : Result.SHUTDOWN_OUT; } case END: if (BufferUtils.hasContent(content)) { if (log.isDebugEnabled()) log.debug("discarding content in COMPLETING"); BufferUtils.clear(content); } return Result.DONE; default: throw new IllegalStateException(); } } public Result generateResponse(MetaData.Response info, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) { return generateResponse(info, false, header, chunk, content, last); } public Result generateResponse(MetaData.Response info, boolean head, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) { switch (_state) { case START: { if (info == null) return Result.NEED_INFO; HttpVersion version = info.getVersion(); if (version == null) throw new BadMessageException(500, "No version"); switch (version) { case HTTP_1_0: if (_persistent == null) _persistent = Boolean.FALSE; break; case HTTP_1_1: if (_persistent == null) _persistent = Boolean.TRUE; break; default: _persistent = false; _endOfContent = EndOfContent.EOF_CONTENT; if (BufferUtils.hasContent(content)) { _contentPrepared += content.remaining(); } _state = last ? State.COMPLETING : State.COMMITTED; return Result.FLUSH; } // Do we need a response header if (header == null) return Result.NEED_HEADER; // prepare the header int pos = BufferUtils.flipToFill(header); try { // generate ResponseLine generateResponseLine(info, header); // Handle 1xx and no content responses int status = info.getStatus(); if (status >= 100 && status < 200) { _noContent = true; if (status != HttpStatus.SWITCHING_PROTOCOLS_101) { header.put(HttpTokens.CRLF); _state = State.COMPLETING_1XX; return Result.FLUSH; } } else if (status == HttpStatus.NO_CONTENT_204 || status == HttpStatus.NOT_MODIFIED_304) { _noContent = true; } generateHeaders(info, header, content, last); // handle the content. int len = BufferUtils.length(content); if (len > 0) { _contentPrepared += len; if (isChunking() && !head) prepareChunk(header, len); } _state = last ? State.COMPLETING : State.COMMITTED; } catch (Exception e) { String message = (e instanceof BufferOverflowException) ? "Response header too large" : e.getMessage(); throw new BadMessageException(500, message, e); } finally { BufferUtils.flipToFlush(header, pos); } return Result.FLUSH; } case COMMITTED: { int len = BufferUtils.length(content); // handle the content. if (len > 0) { if (isChunking()) { if (chunk == null) return Result.NEED_CHUNK; BufferUtils.clearToFill(chunk); prepareChunk(chunk, len); BufferUtils.flipToFlush(chunk, 0); } _contentPrepared += len; } if (last) { _state = State.COMPLETING; return len > 0 ? Result.FLUSH : Result.CONTINUE; } return len > 0 ? Result.FLUSH : Result.DONE; } case COMPLETING_1XX: { reset(); return Result.DONE; } case COMPLETING: { if (BufferUtils.hasContent(content)) { if (log.isDebugEnabled()) log.debug("discarding content in COMPLETING"); BufferUtils.clear(content); } if (isChunking()) { // Do we need a chunk buffer? if (chunk == null) return Result.NEED_CHUNK; // Write the last chunk BufferUtils.clearToFill(chunk); prepareChunk(chunk, 0); BufferUtils.flipToFlush(chunk, 0); _endOfContent = EndOfContent.UNKNOWN_CONTENT; return Result.FLUSH; } _state = State.END; return Boolean.TRUE.equals(_persistent) ? Result.DONE : Result.SHUTDOWN_OUT; } case END: if (BufferUtils.hasContent(content)) { if (log.isDebugEnabled()) log.debug("discarding content in COMPLETING"); BufferUtils.clear(content); } return Result.DONE; default: throw new IllegalStateException(); } } private void prepareChunk(ByteBuffer chunk, int remaining) { // if we need CRLF add this to header if (_needCRLF) BufferUtils.putCRLF(chunk); // Add the chunk size to the header if (remaining > 0) { BufferUtils.putHexInt(chunk, remaining); BufferUtils.putCRLF(chunk); _needCRLF = true; } else { chunk.put(LAST_CHUNK); _needCRLF = false; } } private void generateRequestLine(MetaData.Request request, ByteBuffer header) { header.put(StringUtils.getBytes(request.getMethod())); header.put((byte) ' '); header.put(StringUtils.getBytes(request.getURIString())); header.put((byte) ' '); header.put(request.getVersion().toBytes()); header.put(HttpTokens.CRLF); } private void generateResponseLine(MetaData.Response response, ByteBuffer header) { // Look for prepared response line int status = response.getStatus(); PreparedResponse preprepared = status < __preprepared.length ? __preprepared[status] : null; String reason = response.getReason(); if (preprepared != null) { if (reason == null) header.put(preprepared._responseLine); else { header.put(preprepared._schemeCode); header.put(getReasonBytes(reason)); header.put(HttpTokens.CRLF); } } else // generate response line { header.put(HTTP_1_1_SPACE); header.put((byte) ('0' + status / 100)); header.put((byte) ('0' + (status % 100) / 10)); header.put((byte) ('0' + (status % 10))); header.put((byte) ' '); if (reason == null) { header.put((byte) ('0' + status / 100)); header.put((byte) ('0' + (status % 100) / 10)); header.put((byte) ('0' + (status % 10))); } else header.put(getReasonBytes(reason)); header.put(HttpTokens.CRLF); } } private byte[] getReasonBytes(String reason) { if (reason.length() > 1024) reason = reason.substring(0, 1024); byte[] _bytes = StringUtils.getBytes(reason); for (int i = _bytes.length; i-- > 0;) if (_bytes[i] == '\r' || _bytes[i] == '\n') _bytes[i] = '?'; return _bytes; } private void generateHeaders(MetaData _info, ByteBuffer header, ByteBuffer content, boolean last) { final MetaData.Request request = (_info instanceof MetaData.Request) ? (MetaData.Request) _info : null; final MetaData.Response response = (_info instanceof MetaData.Response) ? (MetaData.Response) _info : null; // default field values int send = _send; HttpField transfer_encoding = null; boolean keep_alive = false; boolean close = false; boolean content_type = false; StringBuilder connection = null; long content_length = _info.getContentLength(); // Generate fields HttpFields fields = _info.getFields(); if (fields != null) { int n = fields.size(); for (int f = 0; f < n; f++) { HttpField field = fields.getField(f); String v = field.getValue(); if (v == null || v.length() == 0) continue; // rfc7230 does not allow no value HttpHeader h = field.getHeader(); if (h == null) putTo(field, header); else { switch (h) { case CONTENT_LENGTH: _endOfContent = EndOfContent.CONTENT_LENGTH; if (content_length < 0) content_length = Long.valueOf(field.getValue()); // handle setting the field specially below break; case CONTENT_TYPE: { // write the field to the header content_type = true; putTo(field, header); break; } case TRANSFER_ENCODING: { if (_info.getVersion() == HttpVersion.HTTP_1_1) transfer_encoding = field; // Do NOT add yet! break; } case CONNECTION: { if (request != null) putTo(field, header); // Lookup and/or split connection value field HttpHeaderValue[] values = HttpHeaderValue.CLOSE.is(field.getValue()) ? CLOSE : new HttpHeaderValue[] { HttpHeaderValue.CACHE.get(field.getValue()) }; String[] split = null; if (values[0] == null) { split = StringUtils.csvSplit(field.getValue()); if (split.length > 0) { values = new HttpHeaderValue[split.length]; for (int i = 0; i < split.length; i++) values[i] = HttpHeaderValue.CACHE.get(split[i]); } } // Handle connection values for (int i = 0; i < values.length; i++) { HttpHeaderValue value = values[i]; switch (value == null ? HttpHeaderValue.UNKNOWN : value) { case UPGRADE: { // special case for websocket connection // ordering header.put(HttpHeader.CONNECTION.getBytesColonSpace()) .put(HttpHeader.UPGRADE.getBytes()); header.put(CRLF); break; } case CLOSE: { close = true; _persistent = false; if (response != null) { if (_endOfContent == EndOfContent.UNKNOWN_CONTENT) _endOfContent = EndOfContent.EOF_CONTENT; } break; } case KEEP_ALIVE: { if (_info.getVersion() == HttpVersion.HTTP_1_0) { keep_alive = true; if (response != null) _persistent = true; } break; } default: { if (connection == null) connection = new StringBuilder(); else connection.append(','); connection.append(split == null ? field.getValue() : split[i]); } } } // Do NOT add yet! break; } case SERVER: { send = send & ~SEND_SERVER; putTo(field, header); break; } default: putTo(field, header); } } } } // Calculate how to end _content and connection, _content length and transfer encoding // settings. // From http://tools.ietf.org/html/rfc7230#section-3.3.3 // From RFC 2616 4.4: // 1. No body for 1xx, 204, 304 & HEAD response // 3. If Transfer-Encoding==(.*,)?chunked && HTTP/1.1 && !HttpConnection==close then chunk // 5. Content-Length without Transfer-Encoding // 6. Request and none over the above, then Content-Length=0 if POST/PUT // 7. close int status = response != null ? response.getStatus() : -1; switch (_endOfContent) { case UNKNOWN_CONTENT: // It may be that we have no _content, or perhaps _content just has not been // written yet? // Response known not to have a body if (_contentPrepared == 0 && response != null && _noContent) _endOfContent = EndOfContent.NO_CONTENT; else if (_info.getContentLength() > 0) { // we have been given a content length _endOfContent = EndOfContent.CONTENT_LENGTH; if ((response != null || content_length > 0 || content_type) && !_noContent) { // known length but not actually set. header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace()); BufferUtils.putDecLong(header, content_length); header.put(HttpTokens.CRLF); } } else if (last) { // we have seen all the _content there is, so we can be // content-length limited. _endOfContent = EndOfContent.CONTENT_LENGTH; long actual_length = _contentPrepared + BufferUtils.length(content); if (content_length >= 0 && content_length != actual_length) throw new BadMessageException(500, "Content-Length header(" + content_length + ") != actual(" + actual_length + ")"); // Do we need to tell the headers about it putContentLength(header, actual_length, content_type, request, response); } else { // No idea, so we must assume that a body is coming. _endOfContent = EndOfContent.CHUNKED_CONTENT; // HTTP 1.0 does not understand chunked content, so we must use // EOF content. // For a request with HTTP 1.0 & Connection: keep-alive // we *must* close the connection, otherwise the client // has no way to detect the end of the content. if (!isPersistent() || _info.getVersion().ordinal() < HttpVersion.HTTP_1_1.ordinal()) _endOfContent = EndOfContent.EOF_CONTENT; } break; case CONTENT_LENGTH: { putContentLength(header, content_length, content_type, request, response); break; } case NO_CONTENT: throw new BadMessageException(500); case EOF_CONTENT: _persistent = request != null; break; case CHUNKED_CONTENT: break; default: break; } // Add transfer_encoding if needed if (isChunking()) { // try to use user supplied encoding as it may have other values. if (transfer_encoding != null && !HttpHeaderValue.CHUNKED.toString().equalsIgnoreCase(transfer_encoding.getValue())) { String c = transfer_encoding.getValue(); if (c.endsWith(HttpHeaderValue.CHUNKED.toString())) putTo(transfer_encoding, header); else throw new BadMessageException(500, "BAD TE"); } else header.put(TRANSFER_ENCODING_CHUNKED); } // Handle connection if need be if (_endOfContent == EndOfContent.EOF_CONTENT) { keep_alive = false; _persistent = false; } // If this is a response, work out persistence if (response != null) { if (!isPersistent() && (close || _info.getVersion().ordinal() > HttpVersion.HTTP_1_0.ordinal())) { if (connection == null) header.put(CONNECTION_CLOSE); else { header.put(CONNECTION_CLOSE, 0, CONNECTION_CLOSE.length - 2); header.put((byte) ','); header.put(StringUtils.getBytes(connection.toString())); header.put(CRLF); } } else if (keep_alive) { if (connection == null) header.put(CONNECTION_KEEP_ALIVE); else { header.put(CONNECTION_KEEP_ALIVE, 0, CONNECTION_KEEP_ALIVE.length - 2); header.put((byte) ','); header.put(StringUtils.getBytes(connection.toString())); header.put(CRLF); } } else if (connection != null) { header.put(HttpHeader.CONNECTION.getBytesColonSpace()); header.put(StringUtils.getBytes(connection.toString())); header.put(CRLF); } } if (status > 199) header.put(SEND[send]); // end the header. header.put(HttpTokens.CRLF); } private void putContentLength(ByteBuffer header, long contentLength, boolean contentType, MetaData.Request request, MetaData.Response response) { if (contentLength > 0) { header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace()); BufferUtils.putDecLong(header, contentLength); header.put(HttpTokens.CRLF); } else if (!_noContent) { if (contentType || response != null || (request != null && __assumedContentMethods.contains(request.getMethod()))) header.put(CONTENT_LENGTH_0); } } public static byte[] getReasonBuffer(int code) { PreparedResponse status = code < __preprepared.length ? __preprepared[code] : null; if (status != null) return status._reason; return null; } @Override public String toString() { return String.format("%s@%x{s=%s}", getClass().getSimpleName(), hashCode(), _state); } // common _content private static final byte[] LAST_CHUNK = { (byte) '0', (byte) '\015', (byte) '\012', (byte) '\015', (byte) '\012' }; private static final byte[] CONTENT_LENGTH_0 = StringUtils.getBytes("Content-Length: 0\015\012"); private static final byte[] CONNECTION_KEEP_ALIVE = StringUtils.getBytes("Connection: keep-alive\015\012"); private static final byte[] CONNECTION_CLOSE = StringUtils.getBytes("Connection: close\015\012"); private static final byte[] HTTP_1_1_SPACE = StringUtils.getBytes(HttpVersion.HTTP_1_1 + " "); private static final byte[] CRLF = StringUtils.getBytes("\015\012"); private static final byte[] TRANSFER_ENCODING_CHUNKED = StringUtils.getBytes("Transfer-Encoding: chunked\015\012"); private static final byte[][] SEND = new byte[][] { new byte[0], StringUtils.getBytes("Server: Firefly 4.0\015\012"), StringUtils.getBytes("X-Powered-By: Firefly 4.0\015\012"), StringUtils.getBytes("Server: Firefly 4.0\015\012X-Powered-By: Firefly 4.0\015\012") }; // Build cache of response lines for status private static class PreparedResponse { byte[] _reason; byte[] _schemeCode; byte[] _responseLine; } private static final PreparedResponse[] __preprepared = new PreparedResponse[HttpStatus.MAX_CODE + 1]; static { int versionLength = HttpVersion.HTTP_1_1.toString().length(); for (int i = 0; i < __preprepared.length; i++) { HttpStatus.Code code = HttpStatus.getCode(i); if (code == null) continue; String reason = code.getMessage(); byte[] line = new byte[versionLength + 5 + reason.length() + 2]; HttpVersion.HTTP_1_1.toBuffer().get(line, 0, versionLength); line[versionLength + 0] = ' '; line[versionLength + 1] = (byte) ('0' + i / 100); line[versionLength + 2] = (byte) ('0' + (i % 100) / 10); line[versionLength + 3] = (byte) ('0' + (i % 10)); line[versionLength + 4] = ' '; for (int j = 0; j < reason.length(); j++) line[versionLength + 5 + j] = (byte) reason.charAt(j); line[versionLength + 5 + reason.length()] = HttpTokens.CARRIAGE_RETURN; line[versionLength + 6 + reason.length()] = HttpTokens.LINE_FEED; __preprepared[i] = new PreparedResponse(); __preprepared[i]._schemeCode = Arrays.copyOfRange(line, 0, versionLength + 5); __preprepared[i]._reason = Arrays.copyOfRange(line, versionLength + 5, line.length - 2); __preprepared[i]._responseLine = line; } } private static void putSanitisedName(String s, ByteBuffer buffer) { int l = s.length(); for (int i = 0; i < l; i++) { char c = s.charAt(i); if (c < 0 || c > 0xff || c == '\r' || c == '\n' || c == ':') buffer.put((byte) '?'); else buffer.put((byte) (0xff & c)); } } private static void putSanitisedValue(String s, ByteBuffer buffer) { int l = s.length(); for (int i = 0; i < l; i++) { char c = s.charAt(i); if (c < 0 || c > 0xff || c == '\r' || c == '\n') buffer.put((byte) ' '); else buffer.put((byte) (0xff & c)); } } public static void putTo(HttpField field, ByteBuffer bufferInFillMode) { if (field instanceof PreEncodedHttpField) { ((PreEncodedHttpField) field).putTo(bufferInFillMode, HttpVersion.HTTP_1_0); } else { HttpHeader header = field.getHeader(); if (header != null) { bufferInFillMode.put(header.getBytesColonSpace()); putSanitisedValue(field.getValue(), bufferInFillMode); } else { putSanitisedName(field.getName(), bufferInFillMode); bufferInFillMode.put(__colon_space); putSanitisedValue(field.getValue(), bufferInFillMode); } BufferUtils.putCRLF(bufferInFillMode); } } public static void putTo(HttpFields fields, ByteBuffer bufferInFillMode) { for (HttpField field : fields) { if (field != null) putTo(field, bufferInFillMode); } BufferUtils.putCRLF(bufferInFillMode); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy