| // |
| // ======================================================================== |
| // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.http; |
| |
| import java.io.IOException; |
| import java.nio.BufferOverflowException; |
| import java.nio.ByteBuffer; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| import org.eclipse.jetty.http.HttpTokens.EndOfContent; |
| import org.eclipse.jetty.util.BufferUtil; |
| import org.eclipse.jetty.util.StringUtil; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| |
| /* ------------------------------------------------------------ */ |
| /** |
| * HttpGenerator. Builds HTTP Messages. |
| * |
| * If the system property "org.eclipse.jetty.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 |
| * </p> |
| */ |
| public class HttpGenerator |
| { |
| private final static Logger LOG = Log.getLogger(HttpGenerator.class); |
| |
| public final static boolean __STRICT=Boolean.getBoolean("org.eclipse.jetty.http.HttpGenerator.STRICT"); |
| |
| private final static byte[] __colon_space = new byte[] {':',' '}; |
| private final static HttpHeaderValue[] CLOSE = {HttpHeaderValue.CLOSE}; |
| public static final ResponseInfo CONTINUE_100_INFO = new ResponseInfo(HttpVersion.HTTP_1_1,null,-1,100,null,false); |
| public static final ResponseInfo PROGRESS_102_INFO = new ResponseInfo(HttpVersion.HTTP_1_1,null,-1,102,null,false); |
| public final static ResponseInfo RESPONSE_500_INFO = |
| new ResponseInfo(HttpVersion.HTTP_1_1,new HttpFields(){{put(HttpHeader.CONNECTION,HttpHeaderValue.CLOSE);}},0,HttpStatus.INTERNAL_SERVER_ERROR_500,null,false); |
| |
| // 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<String> __assumedContentMethods = new HashSet<>(Arrays.asList(new String[]{HttpMethod.POST.asString(),HttpMethod.PUT.asString()})); |
| |
| /* ------------------------------------------------------------------------------- */ |
| public static void setJettyVersion(String serverVersion) |
| { |
| SEND[SEND_SERVER] = StringUtil.getBytes("Server: " + serverVersion + "\015\012"); |
| SEND[SEND_XPOWEREDBY] = StringUtil.getBytes("X-Powered-By: " + serverVersion + "\015\012"); |
| SEND[SEND_SERVER | SEND_XPOWEREDBY] = StringUtil.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; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| @Deprecated |
| public boolean getSendServerVersion () |
| { |
| return (_send&SEND_SERVER)!=0; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| @Deprecated |
| public void setSendServerVersion (boolean sendServerVersion) |
| { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| 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(RequestInfo info, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) throws IOException |
| { |
| switch(_state) |
| { |
| case START: |
| { |
| if (info==null) |
| return Result.NEED_INFO; |
| |
| // Do we need a request header |
| if (header==null) |
| return Result.NEED_HEADER; |
| |
| // If we have not been told our persistence, set the default |
| if (_persistent==null) |
| _persistent=(info.getHttpVersion().ordinal() > HttpVersion.HTTP_1_0.ordinal()); |
| |
| // prepare the header |
| int pos=BufferUtil.flipToFill(header); |
| try |
| { |
| // generate ResponseLine |
| generateRequestLine(info,header); |
| |
| if (info.getHttpVersion()==HttpVersion.HTTP_0_9) |
| _noContent=true; |
| else |
| generateHeaders(info,header,content,last); |
| |
| boolean expect100 = info.getHttpFields().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()); |
| |
| if (expect100) |
| { |
| _state = State.COMMITTED; |
| } |
| else |
| { |
| // handle the content. |
| int len = BufferUtil.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)?"Response header too large":e.getMessage(); |
| throw new IOException(message,e); |
| } |
| finally |
| { |
| BufferUtil.flipToFlush(header,pos); |
| } |
| } |
| |
| case COMMITTED: |
| { |
| int len = BufferUtil.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; |
| BufferUtil.clearToFill(chunk); |
| prepareChunk(chunk,len); |
| BufferUtil.flipToFlush(chunk,0); |
| } |
| _contentPrepared+=len; |
| } |
| |
| if (last) |
| { |
| _state=State.COMPLETING; |
| return len>0?Result.FLUSH:Result.CONTINUE; |
| } |
| |
| return Result.FLUSH; |
| } |
| |
| case COMPLETING: |
| { |
| if (BufferUtil.hasContent(content)) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("discarding content in COMPLETING"); |
| BufferUtil.clear(content); |
| } |
| |
| if (isChunking()) |
| { |
| // Do we need a chunk buffer? |
| if (chunk==null) |
| return Result.NEED_CHUNK; |
| BufferUtil.clearToFill(chunk); |
| prepareChunk(chunk,0); |
| BufferUtil.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 (BufferUtil.hasContent(content)) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("discarding content in COMPLETING"); |
| BufferUtil.clear(content); |
| } |
| return Result.DONE; |
| |
| default: |
| throw new IllegalStateException(); |
| } |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public Result generateResponse(ResponseInfo info, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) throws IOException |
| { |
| switch(_state) |
| { |
| case START: |
| { |
| if (info==null) |
| return Result.NEED_INFO; |
| |
| // Handle 0.9 |
| if (info.getHttpVersion() == HttpVersion.HTTP_0_9) |
| { |
| _persistent = false; |
| _endOfContent=EndOfContent.EOF_CONTENT; |
| if (BufferUtil.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; |
| |
| // If we have not been told our persistence, set the default |
| if (_persistent==null) |
| _persistent=(info.getHttpVersion().ordinal() > HttpVersion.HTTP_1_0.ordinal()); |
| |
| // prepare the header |
| int pos=BufferUtil.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 = BufferUtil.length(content); |
| if (len>0) |
| { |
| _contentPrepared+=len; |
| if (isChunking() && !info.isHead()) |
| 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 IOException(message,e); |
| } |
| finally |
| { |
| BufferUtil.flipToFlush(header,pos); |
| } |
| |
| return Result.FLUSH; |
| } |
| |
| case COMMITTED: |
| { |
| int len = BufferUtil.length(content); |
| |
| // handle the content. |
| if (len>0) |
| { |
| if (isChunking()) |
| { |
| if (chunk==null) |
| return Result.NEED_CHUNK; |
| BufferUtil.clearToFill(chunk); |
| prepareChunk(chunk,len); |
| BufferUtil.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 (BufferUtil.hasContent(content)) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("discarding content in COMPLETING"); |
| BufferUtil.clear(content); |
| } |
| |
| if (isChunking()) |
| { |
| // Do we need a chunk buffer? |
| if (chunk==null) |
| return Result.NEED_CHUNK; |
| |
| // Write the last chunk |
| BufferUtil.clearToFill(chunk); |
| prepareChunk(chunk,0); |
| BufferUtil.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 (BufferUtil.hasContent(content)) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("discarding content in COMPLETING"); |
| BufferUtil.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) |
| BufferUtil.putCRLF(chunk); |
| |
| // Add the chunk size to the header |
| if (remaining>0) |
| { |
| BufferUtil.putHexInt(chunk, remaining); |
| BufferUtil.putCRLF(chunk); |
| _needCRLF=true; |
| } |
| else |
| { |
| chunk.put(LAST_CHUNK); |
| _needCRLF=false; |
| } |
| } |
| |
| /* ------------------------------------------------------------ */ |
| private void generateRequestLine(RequestInfo request,ByteBuffer header) |
| { |
| header.put(StringUtil.getBytes(request.getMethod())); |
| header.put((byte)' '); |
| header.put(StringUtil.getBytes(request.getUri())); |
| switch(request.getHttpVersion()) |
| { |
| case HTTP_1_0: |
| case HTTP_1_1: |
| header.put((byte)' '); |
| header.put(request.getHttpVersion().toBytes()); |
| break; |
| default: |
| throw new IllegalStateException(); |
| } |
| header.put(HttpTokens.CRLF); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| private void generateResponseLine(ResponseInfo 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 = StringUtil.getBytes(reason); |
| |
| for (int i=_bytes.length;i-->0;) |
| if (_bytes[i]=='\r' || _bytes[i]=='\n') |
| _bytes[i]='?'; |
| return _bytes; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| private void generateHeaders(Info _info,ByteBuffer header,ByteBuffer content,boolean last) |
| { |
| final RequestInfo request=(_info instanceof RequestInfo)?(RequestInfo)_info:null; |
| final ResponseInfo response=(_info instanceof ResponseInfo)?(ResponseInfo)_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; |
| |
| // Generate fields |
| if (_info.getHttpFields() != null) |
| { |
| for (HttpField field : _info.getHttpFields()) |
| { |
| HttpHeader h = field.getHeader(); |
| |
| switch (h==null?HttpHeader.UNKNOWN:h) |
| { |
| case CONTENT_LENGTH: |
| // handle specially below |
| if (_info.getContentLength()>=0) |
| _endOfContent=EndOfContent.CONTENT_LENGTH; |
| break; |
| |
| case CONTENT_TYPE: |
| { |
| if (field.getValue().startsWith(MimeTypes.Type.MULTIPART_BYTERANGES.toString())) |
| _endOfContent=EndOfContent.SELF_DEFINING_CONTENT; |
| |
| // write the field to the header |
| content_type=true; |
| putTo(field,header); |
| break; |
| } |
| |
| case TRANSFER_ENCODING: |
| { |
| if (_info.getHttpVersion() == 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 = StringUtil.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; |
| if (response!=null) |
| { |
| _persistent=false; |
| if (_endOfContent == EndOfContent.UNKNOWN_CONTENT) |
| _endOfContent=EndOfContent.EOF_CONTENT; |
| } |
| break; |
| } |
| |
| case KEEP_ALIVE: |
| { |
| if (_info.getHttpVersion() == 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 RFC 2616 4.4: |
| // 1. No body for 1xx, 204, 304 & HEAD response |
| // 2. Force _content-length? |
| // 3. If Transfer-Encoding!=identity && HTTP/1.1 && !HttpConnection==close then chunk |
| // 4. Content-Length |
| // 5. multipart/byteranges |
| // 6. 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 && (status < 200 || status == 204 || status == 304)) |
| _endOfContent=EndOfContent.NO_CONTENT; |
| else if (_info.getContentLength()>0) |
| { |
| // we have been given a content length |
| _endOfContent=EndOfContent.CONTENT_LENGTH; |
| long content_length = _info.getContentLength(); |
| if ((response!=null || content_length>0 || content_type ) && !_noContent) |
| { |
| // known length but not actually set. |
| header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace()); |
| BufferUtil.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 content_length = _contentPrepared+BufferUtil.length(content); |
| |
| // Do we need to tell the headers about it |
| if (content_length>0) |
| { |
| header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace()); |
| BufferUtil.putDecLong(header, content_length); |
| header.put(HttpTokens.CRLF); |
| } |
| else if (!_noContent) |
| { |
| if (content_type || response!=null || (request!=null && __assumedContentMethods.contains(request.getMethod()))) |
| header.put(CONTENT_LENGTH_0); |
| } |
| } |
| 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.getHttpVersion().ordinal() < HttpVersion.HTTP_1_1.ordinal()) |
| _endOfContent = EndOfContent.EOF_CONTENT; |
| } |
| break; |
| |
| case CONTENT_LENGTH: |
| long content_length = _info.getContentLength(); |
| if (content_length>0) |
| { |
| header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace()); |
| BufferUtil.putDecLong(header, content_length); |
| header.put(HttpTokens.CRLF); |
| } |
| else if (!_noContent) |
| { |
| if (content_type || response!=null || (request!=null && __assumedContentMethods.contains(request.getMethod()))) |
| header.put(CONTENT_LENGTH_0); |
| } |
| break; |
| |
| case NO_CONTENT: |
| throw new IllegalStateException(); |
| |
| 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 IllegalArgumentException("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.getHttpVersion().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(StringUtil.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(StringUtil.getBytes(connection.toString())); |
| header.put(CRLF); |
| } |
| } |
| else if (connection!=null) |
| { |
| header.put(HttpHeader.CONNECTION.getBytesColonSpace()); |
| header.put(StringUtil.getBytes(connection.toString())); |
| header.put(CRLF); |
| } |
| } |
| |
| if (status>199) |
| header.put(SEND[send]); |
| |
| // end the header. |
| header.put(HttpTokens.CRLF); |
| } |
| |
| /* ------------------------------------------------------------------------------- */ |
| 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{s=%s}", |
| getClass().getSimpleName(), |
| _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 = StringUtil.getBytes("Content-Length: 0\015\012"); |
| private static final byte[] CONNECTION_KEEP_ALIVE = StringUtil.getBytes("Connection: keep-alive\015\012"); |
| private static final byte[] CONNECTION_CLOSE = StringUtil.getBytes("Connection: close\015\012"); |
| private static final byte[] HTTP_1_1_SPACE = StringUtil.getBytes(HttpVersion.HTTP_1_1+" "); |
| private static final byte[] CRLF = StringUtil.getBytes("\015\012"); |
| private static final byte[] TRANSFER_ENCODING_CHUNKED = StringUtil.getBytes("Transfer-Encoding: chunked\015\012"); |
| private static final byte[][] SEND = new byte[][]{ |
| new byte[0], |
| StringUtil.getBytes("Server: Jetty(9.x.x)\015\012"), |
| StringUtil.getBytes("X-Powered-By: Jetty(9.x.x)\015\012"), |
| StringUtil.getBytes("Server: Jetty(9.x.x)\015\012X-Powered-By: Jetty(9.x.x)\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; |
| } |
| } |
| |
| public static class Info |
| { |
| final HttpVersion _httpVersion; |
| final HttpFields _httpFields; |
| final long _contentLength; |
| |
| private Info(HttpVersion httpVersion, HttpFields httpFields, long contentLength) |
| { |
| _httpVersion = httpVersion; |
| _httpFields = httpFields; |
| _contentLength = contentLength; |
| } |
| |
| public HttpVersion getHttpVersion() |
| { |
| return _httpVersion; |
| } |
| public HttpFields getHttpFields() |
| { |
| return _httpFields; |
| } |
| public long getContentLength() |
| { |
| return _contentLength; |
| } |
| } |
| |
| public static class RequestInfo extends Info |
| { |
| private final String _method; |
| private final String _uri; |
| |
| public RequestInfo(HttpVersion httpVersion, HttpFields httpFields, long contentLength, String method, String uri) |
| { |
| super(httpVersion,httpFields,contentLength); |
| _method = method; |
| _uri = uri; |
| } |
| |
| public String getMethod() |
| { |
| return _method; |
| } |
| |
| public String getUri() |
| { |
| return _uri; |
| } |
| |
| @Override |
| public String toString() |
| { |
| return String.format("RequestInfo{%s %s %s,%d}",_method,_uri,_httpVersion,_contentLength); |
| } |
| } |
| |
| public static class ResponseInfo extends Info |
| { |
| private final int _status; |
| private final String _reason; |
| private final boolean _head; |
| |
| public ResponseInfo(HttpVersion httpVersion, HttpFields httpFields, long contentLength, int status, String reason, boolean head) |
| { |
| super(httpVersion,httpFields,contentLength); |
| _status = status; |
| _reason = reason; |
| _head = head; |
| } |
| |
| public boolean isInformational() |
| { |
| return _status>=100 && _status<200; |
| } |
| |
| public int getStatus() |
| { |
| return _status; |
| } |
| |
| public String getReason() |
| { |
| return _reason; |
| } |
| |
| public boolean isHead() |
| { |
| return _head; |
| } |
| |
| @Override |
| public String toString() |
| { |
| return String.format("ResponseInfo{%s %s %s,%d,%b}",_httpVersion,_status,_reason,_contentLength,_head); |
| } |
| } |
| |
| 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 CachedHttpField) |
| { |
| ((CachedHttpField)field).putTo(bufferInFillMode); |
| } |
| 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); |
| } |
| |
| BufferUtil.putCRLF(bufferInFillMode); |
| } |
| } |
| |
| public static void putTo(HttpFields fields, ByteBuffer bufferInFillMode) |
| { |
| for (HttpField field : fields) |
| { |
| if (field != null) |
| putTo(field,bufferInFillMode); |
| } |
| BufferUtil.putCRLF(bufferInFillMode); |
| } |
| |
| public static class CachedHttpField extends HttpField |
| { |
| private final byte[] _bytes; |
| public CachedHttpField(HttpHeader header,String value) |
| { |
| super(header,value); |
| int cbl=header.getBytesColonSpace().length; |
| _bytes=Arrays.copyOf(header.getBytesColonSpace(), cbl+value.length()+2); |
| System.arraycopy(value.getBytes(StandardCharsets.ISO_8859_1),0,_bytes,cbl,value.length()); |
| _bytes[_bytes.length-2]=(byte)'\r'; |
| _bytes[_bytes.length-1]=(byte)'\n'; |
| } |
| |
| public void putTo(ByteBuffer bufferInFillMode) |
| { |
| bufferInFillMode.put(_bytes); |
| } |
| } |
| } |