de.mklinger.qetcher.client.jetty.http.HttpGenerator Maven / Gradle / Ivy
Show all versions of qetcher-client-bundle Show documentation
//
// ========================================================================
// Copyright (c) 1995-2018 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.util.Arrays;
import java.util.function.Supplier;
import org.eclipse.jetty.http.HttpTokens.EndOfContent;
import org.eclipse.jetty.util.ArrayTrie;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.Trie;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import static org.eclipse.jetty.http.HttpStatus.INTERNAL_SERVER_ERROR_500;
/**
* 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
*/
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[] {':',' '};
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,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 a small chunk buffer of CHUNK_SIZE
NEED_INFO, // Need the request/response metadata info
NEED_HEADER, // Need a buffer to build HTTP headers into
NEED_CHUNK_TRAILER, // Need a large chunk buffer for last chunk and trailers
FLUSH, // The buffers previously generated should be flushed
CONTINUE, // Continue generating the message
SHUTDOWN_OUT, // Need EOF to be signaled
DONE // Message generation complete
}
// 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 _noContentResponse = false;
private Boolean _persistent = null;
private Supplier _trailers = null;
private final int _send;
private final static int SEND_SERVER = 0x01;
private final static int SEND_XPOWEREDBY = 0x02;
private final static Trie __assumedContentMethods = new ArrayTrie<>(8);
static
{
__assumedContentMethods.put(HttpMethod.POST.asString(),Boolean.TRUE);
__assumedContentMethods.put(HttpMethod.PUT.asString(),Boolean.TRUE);
}
/* ------------------------------------------------------------------------------- */
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;
_noContentResponse=false;
_persistent = null;
_contentPrepared = 0;
_needCRLF = false;
_trailers = null;
}
/* ------------------------------------------------------------ */
@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 _noContentResponse;
}
/* ------------------------------------------------------------ */
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) throws IOException
{
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.getHttpVersion().ordinal() > HttpVersion.HTTP_1_0.ordinal();
if (!_persistent && HttpMethod.CONNECT.is(info.getMethod()))
_persistent=true;
}
// prepare the header
int pos=BufferUtil.flipToFill(header);
try
{
// generate ResponseLine
generateRequestLine(info,header);
if (info.getHttpVersion()==HttpVersion.HTTP_0_9)
throw new BadMessageException(INTERNAL_SERVER_ERROR_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 = BufferUtil.length(content);
if (len>0)
{
_contentPrepared+=len;
if (isChunking())
prepareChunk(header,len);
}
_state = last?State.COMPLETING:State.COMMITTED;
}
return Result.FLUSH;
}
catch(BadMessageException e)
{
throw e;
}
catch(BufferOverflowException e)
{
throw new BadMessageException(INTERNAL_SERVER_ERROR_500,"Request header too large",e);
}
catch(Exception e)
{
throw new BadMessageException(INTERNAL_SERVER_ERROR_500,e.getMessage(),e);
}
finally
{
BufferUtil.flipToFlush(header,pos);
}
}
case COMMITTED:
{
return committed(chunk,content,last);
}
case COMPLETING:
{
return completing(chunk,content);
}
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 Result committed( ByteBuffer chunk, ByteBuffer content, boolean last)
{
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;
}
private Result completing( ByteBuffer chunk, ByteBuffer content)
{
if (BufferUtil.hasContent(content))
{
if (LOG.isDebugEnabled())
LOG.debug("discarding content in COMPLETING");
BufferUtil.clear(content);
}
if (isChunking())
{
if (_trailers!=null)
{
// Do we need a chunk buffer?
if (chunk==null || chunk.capacity()<=CHUNK_SIZE)
return Result.NEED_CHUNK_TRAILER;
HttpFields trailers = _trailers.get();
if (trailers!=null)
{
// Write the last chunk
BufferUtil.clearToFill(chunk);
generateTrailers(chunk,trailers);
BufferUtil.flipToFlush(chunk,0);
_endOfContent=EndOfContent.UNKNOWN_CONTENT;
return Result.FLUSH;
}
}
// 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;
}
/* ------------------------------------------------------------ */
@Deprecated
public Result generateResponse(MetaData.Response info, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) throws IOException
{
return generateResponse(info,false,header,chunk,content,last);
}
/* ------------------------------------------------------------ */
public Result generateResponse(MetaData.Response info, boolean head, ByteBuffer header, ByteBuffer chunk, ByteBuffer content, boolean last) throws IOException
{
switch(_state)
{
case START:
{
if (info==null)
return Result.NEED_INFO;
HttpVersion version=info.getHttpVersion();
if (version==null)
throw new BadMessageException(INTERNAL_SERVER_ERROR_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 (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;
// 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 )
{
_noContentResponse=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)
{
_noContentResponse=true;
}
generateHeaders(info,header,content,last);
// handle the content.
int len = BufferUtil.length(content);
if (len>0)
{
_contentPrepared+=len;
if (isChunking() && !head)
prepareChunk(header,len);
}
_state = last?State.COMPLETING:State.COMMITTED;
}
catch(BadMessageException e)
{
throw e;
}
catch(BufferOverflowException e)
{
throw new BadMessageException(INTERNAL_SERVER_ERROR_500,"Response header too large",e);
}
catch(Exception e)
{
throw new BadMessageException(INTERNAL_SERVER_ERROR_500,e.getMessage(),e);
}
finally
{
BufferUtil.flipToFlush(header,pos);
}
return Result.FLUSH;
}
case COMMITTED:
{
return committed(chunk,content,last);
}
case COMPLETING_1XX:
{
reset();
return Result.DONE;
}
case COMPLETING:
{
return completing(chunk,content);
}
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 generateTrailers(ByteBuffer buffer, HttpFields trailer)
{
// if we need CRLF add this to header
if (_needCRLF)
BufferUtil.putCRLF(buffer);
// Add the chunk size to the header
buffer.put(ZERO_CHUNK);
int n=trailer.size();
for (int f=0;f1024)
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(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;
if (LOG.isDebugEnabled())
{
LOG.debug("generateHeaders {} last={} content={}",info,last,BufferUtil.toDetailString(content));
LOG.debug(info.getFields().toString());
}
// default field values
int send=_send;
HttpField transfer_encoding=null;
boolean http11 = info.getHttpVersion() == HttpVersion.HTTP_1_1;
boolean close = false;
_trailers = http11?info.getTrailerSupplier():null;
boolean chunked_hint = _trailers!=null;
boolean content_type = false;
long content_length = info.getContentLength();
boolean content_length_field = false;
// Generate fields
HttpFields fields = info.getFields();
if (fields != null)
{
int n=fields.size();
for (int f=0;f0 || content_length>0)
{
if (_contentPrepared==0 && last)
{
// TODO discard content for backward compatibility with 9.3 releases
// TODO review if it is still needed in 9.4 or can we just throw.
content.clear();
content_length=0;
}
else
throw new BadMessageException(INTERNAL_SERVER_ERROR_500,"Content for no content response");
}
}
// Else if we are HTTP/1.1 and the content length is unknown and we are either persistent
// or it is a request with content (which cannot EOF) or the app has requested chunking
else if (http11 && (chunked_hint || content_length<0 && (_persistent || assumed_content_request)))
{
// we use chunking
_endOfContent = EndOfContent.CHUNKED_CONTENT;
// try to use user supplied encoding as it may have other values.
if (transfer_encoding == null)
header.put(TRANSFER_ENCODING_CHUNKED);
else if (transfer_encoding.toString().endsWith(HttpHeaderValue.CHUNKED.toString()))
{
putTo(transfer_encoding,header);
transfer_encoding = null;
}
else if (!chunked_hint)
{
putTo(new HttpField(HttpHeader.TRANSFER_ENCODING,transfer_encoding.getValue()+",chunked"),header);
transfer_encoding = null;
}
else
throw new BadMessageException(INTERNAL_SERVER_ERROR_500,"Bad Transfer-Encoding");
}
// Else if we known the content length and are a request or a persistent response,
else if (content_length>=0 && (request!=null || _persistent))
{
// Use the content length
_endOfContent = EndOfContent.CONTENT_LENGTH;
putContentLength(header,content_length);
}
// Else if we are a response
else if (response!=null)
{
// We must use EOF - even if we were trying to be persistent
_endOfContent = EndOfContent.EOF_CONTENT;
_persistent=false;
if (content_length>=0 && ( content_length> 0 || assumed_content || content_length_field ))
putContentLength(header,content_length);
if (http11 && !close)
header.put(CONNECTION_CLOSE);
}
// Else we must be a request
else
{
// with no way to indicate body length
throw new BadMessageException(INTERNAL_SERVER_ERROR_500,"Unknown content length for request");
}
if (LOG.isDebugEnabled())
LOG.debug(_endOfContent.toString());
// Add transfer encoding if it is not chunking
if (transfer_encoding!=null)
{
if (chunked_hint)
{
String v = transfer_encoding.getValue();
int c = v.lastIndexOf(',');
if (c>0 && v.lastIndexOf(HttpHeaderValue.CHUNKED.toString(),c)>c)
putTo(new HttpField(HttpHeader.TRANSFER_ENCODING,v.substring(0,c).trim()),header);
}
else
{
putTo(transfer_encoding,header);
}
}
// Send server?
int status=response!=null?response.getStatus():-1;
if (status>199)
header.put(SEND[send]);
// end the header.
header.put(HttpTokens.CRLF);
}
/* ------------------------------------------------------------------------------- */
private static void putContentLength(ByteBuffer header,long contentLength)
{
if (contentLength==0)
header.put(CONTENT_LENGTH_0);
else
{
header.put(HttpHeader.CONTENT_LENGTH.getBytesColonSpace());
BufferUtil.putDecLong(header, contentLength);
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@%x{s=%s}",
getClass().getSimpleName(),
hashCode(),
_state);
}
/* ------------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------------- */
// common _content
private static final byte[] ZERO_CHUNK = { (byte) '0', (byte) '\015', (byte) '\012'};
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_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[] 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;j0xff || 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;i0xff || 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);
}
BufferUtil.putCRLF(bufferInFillMode);
}
}
public static void putTo(HttpFields fields, ByteBuffer bufferInFillMode)
{
for (HttpField field : fields)
{
if (field != null)
putTo(field,bufferInFillMode);
}
BufferUtil.putCRLF(bufferInFillMode);
}
}