Fixes #612 - Support HTTP Trailer.

Implemented support in the generic HttpChannel and Request classes.
Linked HTTP/1.1 and HTTP/2 trailers to call HttpChannel, so that trailers
are now available in Servlet APIs by casting to Jetty's Request class.
The semantic is that trailers will only be available _after_ reading
the request content.
This commit is contained in:
Simone Bordet 2017-01-30 15:17:23 +01:00
parent 4f7c53b9b1
commit 7e7459d825
10 changed files with 340 additions and 74 deletions

View File

@ -497,7 +497,8 @@ public class HttpParser
_cr=true; _cr=true;
if (buffer.hasRemaining()) if (buffer.hasRemaining())
{ {
if(_maxHeaderBytes>0 && _state.ordinal()<State.END.ordinal()) // Don't count the CRs and LFs of the chunked encoding.
if (_maxHeaderBytes>0 && (_state == State.HEADER || _state == State.TRAILER))
_headerBytes++; _headerBytes++;
return next(buffer); return next(buffer);
} }
@ -509,7 +510,6 @@ public class HttpParser
case LEGAL: case LEGAL:
if (_cr) if (_cr)
throw new BadMessageException("Bad EOL"); throw new BadMessageException("Bad EOL");
} }
return ch; return ch;
@ -867,7 +867,6 @@ public class HttpParser
default: default:
throw new IllegalStateException(_state.toString()); throw new IllegalStateException(_state.toString());
} }
} }
@ -993,10 +992,8 @@ public class HttpParser
*/ */
protected boolean parseFields(ByteBuffer buffer) protected boolean parseFields(ByteBuffer buffer)
{ {
boolean handle=false;
// Process headers // Process headers
while ((_state==State.HEADER || _state==State.TRAILER) && buffer.hasRemaining() && !handle) while ((_state==State.HEADER || _state==State.TRAILER) && buffer.hasRemaining())
{ {
// process each character // process each character
byte ch=next(buffer); byte ch=next(buffer);
@ -1005,8 +1002,11 @@ public class HttpParser
if (_maxHeaderBytes>0 && ++_headerBytes>_maxHeaderBytes) if (_maxHeaderBytes>0 && ++_headerBytes>_maxHeaderBytes)
{ {
LOG.warn("Header is too large >"+_maxHeaderBytes); boolean header = _state == State.HEADER;
throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431); LOG.warn("{} is too large {}>{}", header ? "Header" : "Trailer", _headerBytes, _maxHeaderBytes);
throw new BadMessageException(header ?
HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431 :
HttpStatus.PAYLOAD_TOO_LARGE_413);
} }
switch (_fieldState) switch (_fieldState)
@ -1084,29 +1084,34 @@ public class HttpParser
switch (_endOfContent) switch (_endOfContent)
{ {
case EOF_CONTENT: case EOF_CONTENT:
{
setState(State.EOF_CONTENT); setState(State.EOF_CONTENT);
handle=_handler.headerComplete()||handle; boolean handle=_handler.headerComplete();
_headerComplete=true; _headerComplete=true;
return handle; return handle;
}
case CHUNKED_CONTENT: case CHUNKED_CONTENT:
{
setState(State.CHUNKED_CONTENT); setState(State.CHUNKED_CONTENT);
handle=_handler.headerComplete()||handle; boolean handle=_handler.headerComplete();
_headerComplete=true; _headerComplete=true;
return handle; return handle;
}
case NO_CONTENT: case NO_CONTENT:
{
setState(State.END); setState(State.END);
handle=_handler.headerComplete()||handle; boolean handle=_handler.headerComplete();
_headerComplete=true; _headerComplete=true;
handle=_handler.messageComplete()||handle; handle=_handler.messageComplete()||handle;
return handle; return handle;
}
default: default:
{
setState(State.CONTENT); setState(State.CONTENT);
handle=_handler.headerComplete()||handle; boolean handle=_handler.headerComplete();
_headerComplete=true; _headerComplete=true;
return handle; return handle;
}
} }
} }
@ -1315,7 +1320,7 @@ public class HttpParser
} }
} }
return handle; return false;
} }
/* ------------------------------------------------------------------------------- */ /* ------------------------------------------------------------------------------- */
@ -1386,22 +1391,7 @@ public class HttpParser
while (buffer.remaining()>0 && buffer.get(buffer.position())<=HttpTokens.SPACE) while (buffer.remaining()>0 && buffer.get(buffer.position())<=HttpTokens.SPACE)
buffer.get(); buffer.get();
} }
else if (_state==State.CLOSE) else if (isClose() || isClosed())
{
// Seeking EOF
if (BufferUtil.hasContent(buffer))
{
// Just ignore data when closed
_headerBytes+=buffer.remaining();
BufferUtil.clear(buffer);
if (_maxHeaderBytes>0 && _headerBytes>_maxHeaderBytes)
{
// Don't want to waste time reading data of a closed request
throw new IllegalStateException("too much data seeking EOF");
}
}
}
else if (_state==State.CLOSED)
{ {
BufferUtil.clear(buffer); BufferUtil.clear(buffer);
} }
@ -1449,55 +1439,33 @@ public class HttpParser
if (DEBUG) if (DEBUG)
LOG.debug("{} EOF in {}",this,_state); LOG.debug("{} EOF in {}",this,_state);
setState(State.CLOSED); setState(State.CLOSED);
_handler.badMessage(400,null); _handler.badMessage(HttpStatus.BAD_REQUEST_400,null);
break; break;
} }
} }
} }
catch(BadMessageException e) catch(BadMessageException x)
{ {
BufferUtil.clear(buffer); BufferUtil.clear(buffer);
badMessage(x);
Throwable cause = e.getCause();
boolean stack = LOG.isDebugEnabled() ||
(!(cause instanceof NumberFormatException ) && (cause instanceof RuntimeException || cause instanceof Error));
if (stack)
LOG.warn("bad HTTP parsed: "+e._code+(e.getReason()!=null?" "+e.getReason():"")+" for "+_handler,e);
else
LOG.warn("bad HTTP parsed: "+e._code+(e.getReason()!=null?" "+e.getReason():"")+" for "+_handler);
setState(State.CLOSE);
_handler.badMessage(e.getCode(), e.getReason());
} }
catch(NumberFormatException|IllegalStateException e) catch(Throwable x)
{ {
BufferUtil.clear(buffer); BufferUtil.clear(buffer);
LOG.warn("parse exception: {} in {} for {}",e.toString(),_state,_handler); badMessage(new BadMessageException(HttpStatus.BAD_REQUEST_400, _requestHandler != null ? "Bad Request" : "Bad Response", x));
if (DEBUG)
LOG.debug(e);
badMessage();
}
catch(Exception|Error e)
{
BufferUtil.clear(buffer);
LOG.warn("parse exception: "+e.toString()+" for "+_handler,e);
badMessage();
} }
return false; return false;
} }
protected void badMessage() protected void badMessage(BadMessageException x)
{ {
if (DEBUG)
LOG.debug("Parse exception: " + this + " for " + _handler, x);
setState(State.CLOSE);
if (_headerComplete) if (_headerComplete)
{
_handler.earlyEOF(); _handler.earlyEOF();
} else
else if (_state!=State.CLOSED) _handler.badMessage(x._code, x._reason);
{
setState(State.CLOSE);
_handler.badMessage(400,_requestHandler!=null?"Bad Request":"Bad Response");
}
} }
protected boolean parseContent(ByteBuffer buffer) protected boolean parseContent(ByteBuffer buffer)

View File

@ -18,9 +18,17 @@
package org.eclipse.jetty.http2.client; package org.eclipse.jetty.http2.client;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
@ -28,7 +36,9 @@ import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.api.Session; import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream; import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener; import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame; import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.Promise;
@ -83,6 +93,77 @@ public class TrailersTest extends AbstractTest
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
} }
@Test
public void testServletRequestTrailers() throws Exception
{
CountDownLatch trailerLatch = new CountDownLatch(1);
start(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
Request jettyRequest = (Request)request;
// No trailers yet.
Assert.assertNull(jettyRequest.getTrailers());
trailerLatch.countDown();
// Read the content.
ServletInputStream input = jettyRequest.getInputStream();
while (true)
{
int read = input.read();
if (read < 0)
break;
}
// Now we have the trailers.
HttpFields trailers = jettyRequest.getTrailers();
Assert.assertNotNull(trailers);
Assert.assertNotNull(trailers.get("X-Trailer"));
}
});
Session session = newClient(new Session.Listener.Adapter());
HttpFields requestFields = new HttpFields();
requestFields.put("X-Request", "true");
MetaData.Request request = newRequest("GET", requestFields);
HeadersFrame requestFrame = new HeadersFrame(request, null, false);
FuturePromise<Stream> streamPromise = new FuturePromise<>();
CountDownLatch latch = new CountDownLatch(1);
session.newStream(requestFrame, streamPromise, new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
MetaData.Response response = (MetaData.Response)frame.getMetaData();
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
if (frame.isEndStream())
latch.countDown();
}
});
Stream stream = streamPromise.get(5, TimeUnit.SECONDS);
// Send some data.
Callback.Completable callback = new Callback.Completable();
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(16), false), callback);
Assert.assertTrue(trailerLatch.await(5, TimeUnit.SECONDS));
// Send the trailers.
callback.thenRun(() ->
{
HttpFields trailerFields = new HttpFields();
trailerFields.put("X-Trailer", "true");
MetaData trailers = new MetaData(HttpVersion.HTTP_2, trailerFields);
HeadersFrame trailerFrame = new HeadersFrame(stream.getId(), trailers, null, true);
stream.headers(trailerFrame, Callback.NOOP);
});
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@Test @Test
public void testTrailersSentByServer() throws Exception public void testTrailersSentByServer() throws Exception
{ {

View File

@ -167,6 +167,15 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
} }
} }
public void onTrailers(IStream stream, HeadersFrame frame)
{
if (LOG.isDebugEnabled())
LOG.debug("Processing trailers {} on {}", frame, stream);
HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttribute(IStream.CHANNEL_ATTRIBUTE);
if (channel != null)
channel.onRequestTrailers(frame);
}
public boolean onStreamTimeout(IStream stream, Throwable failure) public boolean onStreamTimeout(IStream stream, Throwable failure)
{ {
HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttribute(IStream.CHANNEL_ATTRIBUTE); HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttribute(IStream.CHANNEL_ATTRIBUTE);

View File

@ -142,8 +142,10 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF
@Override @Override
public void onHeaders(Stream stream, HeadersFrame frame) public void onHeaders(Stream stream, HeadersFrame frame)
{ {
// Servers do not receive responses. if (frame.isEndStream())
close(stream, "response_headers"); getConnection().onTrailers((IStream)stream, frame);
else
close(stream, "invalid_trailers");
} }
@Override @Override

View File

@ -274,6 +274,20 @@ public class HttpChannelOverHTTP2 extends HttpChannel
return handle || wasDelayed ? this : null; return handle || wasDelayed ? this : null;
} }
public void onRequestTrailers(HeadersFrame frame)
{
HttpFields trailers = frame.getMetaData().getFields();
onTrailers(trailers);
onRequestComplete();
if (LOG.isDebugEnabled())
{
Stream stream = getStream();
LOG.debug("HTTP2 Request #{}/{}, trailers:{}{}",
stream.getId(), Integer.toHexString(stream.getSession().hashCode()),
System.lineSeparator(), trailers);
}
}
public boolean isRequestHandled() public boolean isRequestHandled()
{ {
return _handled; return _handled;

View File

@ -586,6 +586,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor
return _request.getHttpInput().addContent(content); return _request.getHttpInput().addContent(content);
} }
public void onTrailers(HttpFields trailers)
{
_request.setTrailers(trailers);
}
public boolean onRequestComplete() public boolean onRequestComplete()
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())

View File

@ -62,6 +62,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
private boolean _expect100Continue = false; private boolean _expect100Continue = false;
private boolean _expect102Processing = false; private boolean _expect102Processing = false;
private List<String> _complianceViolations; private List<String> _complianceViolations;
private HttpFields _trailers;
public HttpChannelOverHttp(HttpConnection httpConnection, Connector connector, HttpConfiguration config, EndPoint endPoint, HttpTransport transport) public HttpChannelOverHttp(HttpConnection httpConnection, Connector connector, HttpConfiguration config, EndPoint endPoint, HttpTransport transport)
{ {
@ -87,6 +88,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
_connection = null; _connection = null;
_fields.clear(); _fields.clear();
_upgrade = null; _upgrade = null;
_trailers = null;
} }
@Override @Override
@ -187,6 +189,14 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
_fields.add(field); _fields.add(field);
} }
@Override
public void parsedTrailer(HttpField field)
{
if (_trailers == null)
_trailers = new HttpFields();
_trailers.add(field);
}
/** /**
* If the associated response has the Expect header set to 100 Continue, * If the associated response has the Expect header set to 100 Continue,
* then accessing the input stream indicates that the handler/servlet * then accessing the input stream indicates that the handler/servlet
@ -460,6 +470,8 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
@Override @Override
public boolean messageComplete() public boolean messageComplete()
{ {
if (_trailers != null)
onTrailers(_trailers);
boolean handle = onRequestComplete() || _delayedForContent; boolean handle = onRequestComplete() || _delayedForContent;
_delayedForContent = false; _delayedForContent = false;
return handle; return handle;

View File

@ -416,10 +416,13 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http
// Reset the channel, parsers and generator // Reset the channel, parsers and generator
_channel.recycle(); _channel.recycle();
if (_generator.isPersistent() && !_parser.isClosed()) if (!_parser.isClosed())
_parser.reset(); {
else if (_generator.isPersistent())
_parser.close(); _parser.reset();
else
_parser.close();
}
// Not in a race here with onFillable, because it has given up control before calling handle. // Not in a race here with onFillable, because it has given up control before calling handle.
// in a slight race with #completed, but not sure what to do with that anyway. // in a slight race with #completed, but not sure what to do with that anyway.

View File

@ -166,14 +166,11 @@ public class Request implements HttpServletRequest
private final HttpChannel _channel; private final HttpChannel _channel;
private final List<ServletRequestAttributeListener> _requestAttributeListeners=new ArrayList<>(); private final List<ServletRequestAttributeListener> _requestAttributeListeners=new ArrayList<>();
private final HttpInput _input; private final HttpInput _input;
private MetaData.Request _metaData; private MetaData.Request _metaData;
private String _originalURI; private String _originalURI;
private String _contextPath; private String _contextPath;
private String _servletPath; private String _servletPath;
private String _pathInfo; private String _pathInfo;
private boolean _secure; private boolean _secure;
private String _asyncNotSupportedSource = null; private String _asyncNotSupportedSource = null;
private boolean _newContext; private boolean _newContext;
@ -202,6 +199,7 @@ public class Request implements HttpServletRequest
private long _timeStamp; private long _timeStamp;
private MultiPartInputStreamParser _multiPartInputStream; //if the request is a multi-part mime private MultiPartInputStreamParser _multiPartInputStream; //if the request is a multi-part mime
private AsyncContextState _async; private AsyncContextState _async;
private HttpFields _trailers;
/* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */
public Request(HttpChannel channel, HttpInput input) public Request(HttpChannel channel, HttpInput input)
@ -217,6 +215,11 @@ public class Request implements HttpServletRequest
return metadata==null?null:metadata.getFields(); return metadata==null?null:metadata.getFields();
} }
public HttpFields getTrailers()
{
return _trailers;
}
/* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */
public HttpInput getHttpInput() public HttpInput getHttpInput()
{ {
@ -1847,6 +1850,7 @@ public class Request implements HttpServletRequest
_multiPartInputStream = null; _multiPartInputStream = null;
_remote=null; _remote=null;
_input.recycle(); _input.recycle();
_trailers = null;
} }
/* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */
@ -2215,6 +2219,11 @@ public class Request implements HttpServletRequest
_scope = scope; _scope = scope;
} }
public void setTrailers(HttpFields trailers)
{
_trailers = trailers;
}
/* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */
@Override @Override
public AsyncContext startAsync() throws IllegalStateException public AsyncContext startAsync() throws IllegalStateException

View File

@ -0,0 +1,163 @@
//
// ========================================================================
// Copyright (c) 1995-2017 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.server;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
public class HttpTrailersTest
{
private Server server;
private ServerConnector connector;
private void start(Handler handler) throws Exception
{
server = new Server();
connector = new ServerConnector(server);
server.addConnector(connector);
server.setHandler(handler);
server.start();
}
@After
public void dispose() throws Exception
{
if (server != null)
server.stop();
}
@Test
public void testServletRequestTrailers() throws Exception
{
String trailerName = "Trailer";
String trailerValue = "value";
start(new AbstractHandler.ErrorDispatchHandler()
{
@Override
protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
jettyRequest.setHandled(true);
// Read the content first.
ServletInputStream input = jettyRequest.getInputStream();
while (true)
{
int read = input.read();
if (read < 0)
break;
}
// Now the trailers can be accessed.
HttpFields trailers = jettyRequest.getTrailers();
Assert.assertNotNull(trailers);
Assert.assertEquals(trailerValue, trailers.get(trailerName));
}
});
try (Socket client = new Socket("localhost", connector.getLocalPort()))
{
client.setSoTimeout(5000);
String request = "" +
"GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Transfer-Encoding: chunked\r\n" +
"\r\n" +
"0\r\n" +
trailerName + ": " + trailerValue + "\r\n" +
"\r\n";
OutputStream output = client.getOutputStream();
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client.getInputStream()));
Assert.assertNotNull(response);
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
@Test
public void testHugeTrailer() throws Exception
{
start(new AbstractHandler.ErrorDispatchHandler()
{
@Override
protected void doNonErrorHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
jettyRequest.setHandled(true);
try
{
// EOF will not be reached because of the huge trailer.
ServletInputStream input = jettyRequest.getInputStream();
while (true)
{
int read = input.read();
if (read < 0)
break;
}
Assert.fail();
}
catch (IOException x)
{
// Expected.
}
}
});
char[] huge = new char[1024 * 1024];
Arrays.fill(huge, 'X');
try (Socket client = new Socket("localhost", connector.getLocalPort()))
{
client.setSoTimeout(5000);
String request = "" +
"GET / HTTP/1.1\r\n" +
"Host: localhost\r\n" +
"Transfer-Encoding: chunked\r\n" +
"\r\n" +
"0\r\n" +
"Trailer: " + new String(huge) + "\r\n" +
"\r\n";
OutputStream output = client.getOutputStream();
output.write(request.getBytes(StandardCharsets.UTF_8));
output.flush();
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client.getInputStream()));
Assert.assertNotNull(response);
Assert.assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
}