Merge remote-tracking branch 'origin/jetty-9.3.x' into jetty-9.4.x

This commit is contained in:
Jan Bartel 2016-07-28 15:06:12 +10:00
commit e45e649761
7 changed files with 737 additions and 2 deletions

View File

@ -0,0 +1,322 @@
//
// ========================================================================
// 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.server.handler;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.server.HttpOutput.Interceptor;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IncludeExclude;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* Buffered Response Handler
* <p>
* A Handler that can apply a {@link org.eclipse.jetty.server.HttpOutput.Interceptor}
* mechanism to buffer the entire response content until the output is closed.
* This allows the commit to be delayed until the response is complete and thus
* headers and response status can be changed while writing the body.
* <p>
* Note that the decision to buffer is influenced by the headers and status at the
* first write, and thus subsequent changes to those headers will not influence the
* decision to buffer or not.
* <p>
* Note also that there are no memory limits to the size of the buffer, thus
* this handler can represent an unbounded memory commitment if the content
* generated can also be unbounded.
* </p>
*/
public class BufferedResponseHandler extends HandlerWrapper
{
static final Logger LOG = Log.getLogger(BufferedResponseHandler.class);
private final IncludeExclude<String> _methods = new IncludeExclude<>();
private final IncludeExclude<String> _paths = new IncludeExclude<>(PathSpecSet.class);
private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>();
/* ------------------------------------------------------------ */
public BufferedResponseHandler()
{
// include only GET requests
_methods.include(HttpMethod.GET.asString());
// Exclude images, aduio and video from buffering
for (String type:MimeTypes.getKnownMimeTypes())
{
if (type.startsWith("image/")||
type.startsWith("audio/")||
type.startsWith("video/"))
_mimeTypes.exclude(type);
}
LOG.debug("{} mime types {}",this,_mimeTypes);
}
/* ------------------------------------------------------------ */
public IncludeExclude<String> getMethodIncludeExclude()
{
return _methods;
}
/* ------------------------------------------------------------ */
public IncludeExclude<String> getPathIncludeExclude()
{
return _paths;
}
/* ------------------------------------------------------------ */
public IncludeExclude<String> getMimeIncludeExclude()
{
return _mimeTypes;
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
ServletContext context = baseRequest.getServletContext();
String path = context==null?baseRequest.getRequestURI():URIUtil.addPaths(baseRequest.getServletPath(),baseRequest.getPathInfo());
LOG.debug("{} handle {} in {}",this,baseRequest,context);
HttpOutput out = baseRequest.getResponse().getHttpOutput();
// Are we already being gzipped?
HttpOutput.Interceptor interceptor = out.getInterceptor();
while (interceptor!=null)
{
if (interceptor instanceof BufferedInterceptor)
{
LOG.debug("{} already intercepting {}",this,request);
_handler.handle(target,baseRequest, request, response);
return;
}
interceptor=interceptor.getNextInterceptor();
}
// If not a supported method - no Vary because no matter what client, this URI is always excluded
if (!_methods.matches(baseRequest.getMethod()))
{
LOG.debug("{} excluded by method {}",this,request);
_handler.handle(target,baseRequest, request, response);
return;
}
// If not a supported URI- no Vary because no matter what client, this URI is always excluded
// Use pathInfo because this is be
if (!isPathBufferable(path))
{
LOG.debug("{} excluded by path {}",this,request);
_handler.handle(target,baseRequest, request, response);
return;
}
// If the mime type is known from the path, then apply mime type filtering
String mimeType = context==null?null:context.getMimeType(path);
if (mimeType!=null)
{
mimeType = MimeTypes.getContentTypeWithoutCharset(mimeType);
if (!isMimeTypeBufferable(mimeType))
{
LOG.debug("{} excluded by path suffix mime type {}",this,request);
// handle normally without setting vary header
_handler.handle(target,baseRequest, request, response);
return;
}
}
// install interceptor and handle
out.setInterceptor(new BufferedInterceptor(baseRequest.getHttpChannel(),out.getInterceptor()));
if (_handler!=null)
_handler.handle(target,baseRequest, request, response);
}
/* ------------------------------------------------------------ */
protected boolean isMimeTypeBufferable(String mimetype)
{
return _mimeTypes.matches(mimetype);
}
/* ------------------------------------------------------------ */
protected boolean isPathBufferable(String requestURI)
{
if (requestURI == null)
return true;
return _paths.matches(requestURI);
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
private class BufferedInterceptor implements HttpOutput.Interceptor
{
final Interceptor _next;
final HttpChannel _channel;
final Queue<ByteBuffer> _buffers=new ConcurrentLinkedQueue<>();
Boolean _aggregating;
ByteBuffer _aggregate;
public BufferedInterceptor(HttpChannel httpChannel, Interceptor interceptor)
{
_next=interceptor;
_channel=httpChannel;
}
@Override
public void write(ByteBuffer content, boolean last, Callback callback)
{
if (LOG.isDebugEnabled())
LOG.debug("{} write last={} {}",this,last,BufferUtil.toDetailString(content));
// if we are not committed, have to decide if we should aggregate or not
if (_aggregating==null)
{
Response response = _channel.getResponse();
int sc = response.getStatus();
if (sc>0 && (sc<200 || sc==204 || sc==205 || sc>=300))
_aggregating=Boolean.FALSE; // No body
else
{
String ct = response.getContentType();
if (ct==null)
_aggregating=Boolean.TRUE;
else
{
ct=MimeTypes.getContentTypeWithoutCharset(ct);
_aggregating=isMimeTypeBufferable(StringUtil.asciiToLowerCase(ct));
}
}
}
// If we are not aggregating, then handle normally
if (!_aggregating.booleanValue())
{
getNextInterceptor().write(content,last,callback);
return;
}
// If last
if (last)
{
// Add the current content to the buffer list without a copy
if (BufferUtil.length(content)>0)
_buffers.add(content);
if (LOG.isDebugEnabled())
LOG.debug("{} committing {}",this,_buffers.size());
commit(_buffers,callback);
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("{} aggregating",this);
// Aggregate the content into buffer chain
while (BufferUtil.hasContent(content))
{
// Do we need a new aggregate buffer
if (BufferUtil.space(_aggregate)==0)
{
int size = Math.max(_channel.getHttpConfiguration().getOutputBufferSize(),BufferUtil.length(content));
_aggregate=BufferUtil.allocate(size); // TODO use a buffer pool
_buffers.add(_aggregate);
}
BufferUtil.append(_aggregate,content);
}
callback.succeeded();
}
}
@Override
public Interceptor getNextInterceptor()
{
return _next;
}
@Override
public boolean isOptimizedForDirectBuffers()
{
return false;
}
protected void commit(Queue<ByteBuffer> buffers, Callback callback)
{
// If only 1 buffer
if (_buffers.size()==1)
// just flush it with the last callback
getNextInterceptor().write(_buffers.remove(),true,callback);
else
{
// Create an iterating callback to do the writing
IteratingCallback icb = new IteratingCallback()
{
@Override
protected Action process() throws Exception
{
ByteBuffer buffer = _buffers.poll();
if (buffer==null)
return Action.SUCCEEDED;
getNextInterceptor().write(buffer,_buffers.isEmpty(),this);
return Action.SCHEDULED;
}
@Override
protected void onCompleteSuccess()
{
// Signal last callback
callback.succeeded();
}
@Override
protected void onCompleteFailure(Throwable cause)
{
// Signal last callback
callback.failed(cause);
}
};
icb.iterate();
}
}
}
}

View File

@ -0,0 +1,239 @@
//
// ========================================================================
// 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.server.handler;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
import java.io.IOException;
import java.util.Arrays;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.LocalConnector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Resource Handler test
*
* TODO: increase the testing going on here
*/
public class BufferedResponseHandlerTest
{
private static Server _server;
private static HttpConfiguration _config;
private static LocalConnector _local;
private static ContextHandler _contextHandler;
private static BufferedResponseHandler _bufferedHandler;
private static TestHandler _test;
@BeforeClass
public static void setUp() throws Exception
{
_server = new Server();
_config = new HttpConfiguration();
_config.setOutputBufferSize(1024);
_config.setOutputAggregationSize(256);
_local = new LocalConnector(_server,new HttpConnectionFactory(_config));
_server.addConnector(_local);
_bufferedHandler = new BufferedResponseHandler();
_bufferedHandler.getPathIncludeExclude().include("/include/*");
_bufferedHandler.getPathIncludeExclude().exclude("*.exclude");
_bufferedHandler.getMimeIncludeExclude().exclude("text/excluded");
_bufferedHandler.setHandler(_test=new TestHandler());
_contextHandler = new ContextHandler("/ctx");
_contextHandler.setHandler(_bufferedHandler);
_server.setHandler(_contextHandler);
_server.start();
// BufferedResponseHandler.LOG.setDebugEnabled(true);
}
@AfterClass
public static void tearDown() throws Exception
{
_server.stop();
}
@Before
public void before()
{
_test._bufferSize=-1;
_test._mimeType=null;
_test._content=new byte[128];
Arrays.fill(_test._content,(byte)'X');
_test._content[_test._content.length-1]='\n';
_test._writes=10;
_test._flush=false;
_test._close=false;
}
@Test
public void testNormal() throws Exception
{
String response = _local.getResponse("GET /ctx/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
assertThat(response,containsString(" 200 OK"));
assertThat(response,containsString("Write: 0"));
assertThat(response,containsString("Write: 7"));
assertThat(response,not(containsString("Content-Length: ")));
assertThat(response,not(containsString("Write: 8")));
assertThat(response,not(containsString("Write: 9")));
assertThat(response,not(containsString("Written: true")));
}
@Test
public void testIncluded() throws Exception
{
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
assertThat(response,containsString(" 200 OK"));
assertThat(response,containsString("Write: 0"));
assertThat(response,containsString("Write: 9"));
assertThat(response,containsString("Written: true"));
}
@Test
public void testExcludedByPath() throws Exception
{
String response = _local.getResponse("GET /ctx/include/path.exclude HTTP/1.1\r\nHost: localhost\r\n\r\n");
assertThat(response,containsString(" 200 OK"));
assertThat(response,containsString("Write: 0"));
assertThat(response,containsString("Write: 7"));
assertThat(response,not(containsString("Content-Length: ")));
assertThat(response,not(containsString("Write: 8")));
assertThat(response,not(containsString("Write: 9")));
assertThat(response,not(containsString("Written: true")));
}
@Test
public void testExcludedByMime() throws Exception
{
_test._mimeType="text/excluded";
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
assertThat(response,containsString(" 200 OK"));
assertThat(response,containsString("Write: 0"));
assertThat(response,containsString("Write: 7"));
assertThat(response,not(containsString("Content-Length: ")));
assertThat(response,not(containsString("Write: 8")));
assertThat(response,not(containsString("Write: 9")));
assertThat(response,not(containsString("Written: true")));
}
@Test
public void testFlushed() throws Exception
{
_test._flush=true;
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
assertThat(response,containsString(" 200 OK"));
assertThat(response,containsString("Write: 0"));
assertThat(response,containsString("Write: 9"));
assertThat(response,containsString("Written: true"));
}
@Test
public void testClosed() throws Exception
{
_test._close=true;
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
assertThat(response,containsString(" 200 OK"));
assertThat(response,containsString("Write: 0"));
assertThat(response,containsString("Write: 9"));
assertThat(response,not(containsString("Written: true")));
}
@Test
public void testBufferSizeSmall() throws Exception
{
_test._bufferSize=16;
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
assertThat(response,containsString(" 200 OK"));
assertThat(response,containsString("Write: 0"));
assertThat(response,containsString("Write: 9"));
assertThat(response,containsString("Written: true"));
}
@Test
public void testBufferSizeBig() throws Exception
{
_test._bufferSize=4096;
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
assertThat(response,containsString(" 200 OK"));
assertThat(response,containsString("Content-Length: "));
assertThat(response,containsString("Write: 0"));
assertThat(response,containsString("Write: 9"));
assertThat(response,containsString("Written: true"));
}
@Test
public void testOne() throws Exception
{
_test._writes=1;
String response = _local.getResponse("GET /ctx/include/path HTTP/1.1\r\nHost: localhost\r\n\r\n");
assertThat(response,containsString(" 200 OK"));
assertThat(response,containsString("Content-Length: "));
assertThat(response,containsString("Write: 0"));
assertThat(response,not(containsString("Write: 1")));
assertThat(response,containsString("Written: true"));
}
public static class TestHandler extends AbstractHandler
{
int _bufferSize;
String _mimeType;
byte[] _content;
int _writes;
boolean _flush;
boolean _close;
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
if (_bufferSize>0)
response.setBufferSize(_bufferSize);
if (_mimeType!=null)
response.setContentType(_mimeType);
for (int i=0;i<_writes;i++)
{
response.addHeader("Write",Integer.toString(i));
response.getOutputStream().write(_content);
if (_flush)
response.getOutputStream().flush();
}
if (_close)
response.getOutputStream().close();
response.addHeader("Written","true");
}
}
}

View File

@ -36,6 +36,8 @@ import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.pathmap.MappedResource;
import org.eclipse.jetty.http.pathmap.PathMappings;
import org.eclipse.jetty.http.pathmap.PathSpec;
import org.eclipse.jetty.http.pathmap.RegexPathSpec;
import org.eclipse.jetty.http.pathmap.ServletPathSpec;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.servlet.FilterHolder;
@ -143,6 +145,26 @@ public class WebSocketUpgradeFilter extends ContainerLifeCycle implements Filter
pathmap.put(spec,creator);
}
/**
* @deprecated use new {@link #addMapping(org.eclipse.jetty.http.pathmap.PathSpec, WebSocketCreator)} instead
*/
@Deprecated
public void addMapping(org.eclipse.jetty.websocket.server.pathmap.PathSpec spec, WebSocketCreator creator)
{
if (spec instanceof org.eclipse.jetty.websocket.server.pathmap.ServletPathSpec)
{
addMapping(new ServletPathSpec(spec.getSpec()), creator);
}
else if (spec instanceof org.eclipse.jetty.websocket.server.pathmap.RegexPathSpec)
{
addMapping(new RegexPathSpec(spec.getSpec()), creator);
}
else
{
throw new RuntimeException("Unsupported (Deprecated) PathSpec implementation: " + spec.getClass().getName());
}
}
@Override
public void destroy()
{

View File

@ -0,0 +1,38 @@
//
// ========================================================================
// 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.websocket.server.pathmap;
/**
* @deprecated moved to jetty-http {@link org.eclipse.jetty.http.pathmap.PathSpec} (this facade will be removed in Jetty 9.4)
*/
@Deprecated
public abstract class PathSpec
{
private final String spec;
protected PathSpec(String spec)
{
this.spec = spec;
}
public String getSpec()
{
return spec;
}
}

View File

@ -22,7 +22,7 @@ package org.eclipse.jetty.websocket.server.pathmap;
* @deprecated moved to jetty-http {@link org.eclipse.jetty.http.pathmap.RegexPathSpec} (this facade will be removed in Jetty 9.4)
*/
@Deprecated
public class RegexPathSpec extends org.eclipse.jetty.http.pathmap.RegexPathSpec
public class RegexPathSpec extends PathSpec
{
public RegexPathSpec(String regex)
{

View File

@ -22,7 +22,7 @@ package org.eclipse.jetty.websocket.server.pathmap;
* @deprecated moved to jetty-http {@link org.eclipse.jetty.http.pathmap.ServletPathSpec} (this facade will be removed in Jetty 9.4)
*/
@Deprecated
public class ServletPathSpec extends org.eclipse.jetty.http.pathmap.ServletPathSpec
public class ServletPathSpec extends PathSpec
{
public ServletPathSpec(String servletPathSpec)
{

View File

@ -0,0 +1,114 @@
//
// ========================================================================
// 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.server.session;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.junit.Test;
/**
* RedirectSessionTest
*
* Test that creating a session and then doing a redirect preserves the session.
*/
public class RedirectSessionTest
{
@Test
public void testSessionRedirect() throws Exception
{
AbstractTestServer testServer = new HashTestServer(0);
ServletContextHandler testServletContextHandler = testServer.addContext("/context");
testServletContextHandler.addServlet(Servlet1.class, "/one");
testServletContextHandler.addServlet(Servlet2.class, "/two");
try
{
testServer.start();
int serverPort=testServer.getPort();
HttpClient client = new HttpClient();
client.setFollowRedirects(true); //ensure client handles redirects
client.start();
try
{
//make a request to the first servlet, which will redirect
ContentResponse response = client.GET("http://localhost:" + serverPort + "/context/one");
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
}
finally
{
client.stop();
}
}
finally
{
testServer.stop();
}
}
public static class Servlet1 extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
//create a session
HttpSession session = request.getSession(true);
assertNotNull(session);
session.setAttribute("servlet1", "servlet1");
response.sendRedirect("/context/two");
}
}
public static class Servlet2 extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
//the session should exist after the redirect
HttpSession sess = request.getSession(false);
assertNotNull(sess);
assertNotNull(sess.getAttribute("servlet1"));
assertEquals("servlet1", sess.getAttribute("servlet1"));
}
}
}