Merge branch 'master' into jetty-8

This commit is contained in:
Jesse McConnell 2012-07-11 10:10:00 -05:00
commit 0fa17c13b1
26 changed files with 1486 additions and 278 deletions

View File

@ -1,4 +1,16 @@
package org.eclipse.jetty.io; package org.eclipse.jetty.io;
//========================================================================
//Copyright (c) 2006-2012 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.
//========================================================================
import org.eclipse.jetty.io.nio.DirectNIOBuffer; import org.eclipse.jetty.io.nio.DirectNIOBuffer;
import org.eclipse.jetty.io.nio.IndirectNIOBuffer; import org.eclipse.jetty.io.nio.IndirectNIOBuffer;

View File

@ -1,4 +1,16 @@
package org.eclipse.jetty.io; package org.eclipse.jetty.io;
//========================================================================
//Copyright (c) 2006-2009 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.
//========================================================================
import java.io.IOException; import java.io.IOException;

View File

@ -333,9 +333,10 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPo
if (l==0 && ( header!=null && header.hasContent() || buffer!=null && buffer.hasContent() || trailer!=null && trailer.hasContent())) if (l==0 && ( header!=null && header.hasContent() || buffer!=null && buffer.hasContent() || trailer!=null && trailer.hasContent()))
{ {
synchronized (this) synchronized (this)
{ {
if (_dispatched) _writable=false;
_writable=false; if (!_dispatched)
updateKey();
} }
} }
else if (l>0) else if (l>0)
@ -358,9 +359,10 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPo
if (l==0 && buffer!=null && buffer.hasContent()) if (l==0 && buffer!=null && buffer.hasContent())
{ {
synchronized (this) synchronized (this)
{ {
if (_dispatched) _writable=false;
_writable=false; if (!_dispatched)
updateKey();
} }
} }
else if (l>0) else if (l>0)

View File

@ -354,7 +354,7 @@ public class Server extends HandlerWrapper implements Attributes
{ {
LOG.debug("REQUEST "+target+" on "+connection); LOG.debug("REQUEST "+target+" on "+connection);
handle(target, request, request, response); handle(target, request, request, response);
LOG.debug("RESPONSE "+target+" "+connection.getResponse().getStatus()); LOG.debug("RESPONSE "+target+" "+connection.getResponse().getStatus()+" handled="+request.isHandled());
} }
else else
handle(target, request, request, response); handle(target, request, request, response);

View File

@ -233,21 +233,21 @@ public class ConnectHandler extends HandlerWrapper
} }
catch (SocketException se) catch (SocketException se)
{ {
LOG.info("ConnectHandler: " + se.getMessage()); LOG.info("ConnectHandler: SocketException " + se.getMessage());
response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
baseRequest.setHandled(true); baseRequest.setHandled(true);
return; return;
} }
catch (SocketTimeoutException ste) catch (SocketTimeoutException ste)
{ {
LOG.info("ConnectHandler: " + ste.getMessage()); LOG.info("ConnectHandler: SocketTimeoutException" + ste.getMessage());
response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT); response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT);
baseRequest.setHandled(true); baseRequest.setHandled(true);
return; return;
} }
catch (IOException ioe) catch (IOException ioe)
{ {
LOG.info("ConnectHandler: " + ioe.getMessage()); LOG.info("ConnectHandler: IOException" + ioe.getMessage());
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
baseRequest.setHandled(true); baseRequest.setHandled(true);
return; return;

View File

@ -0,0 +1,417 @@
// ========================================================================
// Copyright (c) 2012 Intalio, Inc.
// ------------------------------------------------------------------------
// 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.servlets;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.server.Request;
/**
* 6
*/
public class BalancerServlet extends ProxyServlet
{
private static final class BalancerMember
{
private String _name;
private String _proxyTo;
private HttpURI _backendURI;
public BalancerMember(String name, String proxyTo)
{
super();
_name = name;
_proxyTo = proxyTo;
_backendURI = new HttpURI(_proxyTo);
}
public String getProxyTo()
{
return _proxyTo;
}
public HttpURI getBackendURI()
{
return _backendURI;
}
@Override
public String toString()
{
return "BalancerMember [_name=" + _name + ", _proxyTo=" + _proxyTo + "]";
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + ((_name == null)?0:_name.hashCode());
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
BalancerMember other = (BalancerMember)obj;
if (_name == null)
{
if (other._name != null)
return false;
}
else if (!_name.equals(other._name))
return false;
return true;
}
}
private static final class RoundRobinIterator implements Iterator<BalancerMember>
{
private BalancerMember[] _balancerMembers;
private AtomicInteger _index;
public RoundRobinIterator(Collection<BalancerMember> balancerMembers)
{
_balancerMembers = (BalancerMember[])balancerMembers.toArray(new BalancerMember[balancerMembers.size()]);
_index = new AtomicInteger(-1);
}
public boolean hasNext()
{
return true;
}
public BalancerMember next()
{
BalancerMember balancerMember = null;
while (balancerMember == null)
{
int currentIndex = _index.get();
int nextIndex = (currentIndex + 1) % _balancerMembers.length;
if (_index.compareAndSet(currentIndex,nextIndex))
{
balancerMember = _balancerMembers[nextIndex];
}
}
return balancerMember;
}
public void remove()
{
throw new UnsupportedOperationException();
}
}
private static final String BALANCER_MEMBER_PREFIX = "BalancerMember.";
private static final List<String> FORBIDDEN_CONFIG_PARAMETERS;
static
{
List<String> params = new LinkedList<String>();
params.add("HostHeader");
params.add("whiteList");
params.add("blackList");
FORBIDDEN_CONFIG_PARAMETERS = Collections.unmodifiableList(params);
}
private static final List<String> REVERSE_PROXY_HEADERS;
static
{
List<String> params = new LinkedList<String>();
params.add("Location");
params.add("Content-Location");
params.add("URI");
REVERSE_PROXY_HEADERS = Collections.unmodifiableList(params);
}
private static final String JSESSIONID = "jsessionid";
private static final String JSESSIONID_URL_PREFIX = JSESSIONID + "=";
private boolean _stickySessions;
private Set<BalancerMember> _balancerMembers = new HashSet<BalancerMember>();
private boolean _proxyPassReverse;
private RoundRobinIterator _roundRobinIterator;
@Override
public void init(ServletConfig config) throws ServletException
{
validateConfig(config);
super.init(config);
initStickySessions(config);
initBalancers(config);
initProxyPassReverse(config);
postInit();
}
private void validateConfig(ServletConfig config) throws ServletException
{
@SuppressWarnings("unchecked")
List<String> initParameterNames = Collections.list(config.getInitParameterNames());
for (String initParameterName : initParameterNames)
{
if (FORBIDDEN_CONFIG_PARAMETERS.contains(initParameterName))
{
throw new UnavailableException(initParameterName + " not supported in " + getClass().getName());
}
}
}
private void initStickySessions(ServletConfig config) throws ServletException
{
_stickySessions = "true".equalsIgnoreCase(config.getInitParameter("StickySessions"));
}
private void initBalancers(ServletConfig config) throws ServletException
{
Set<String> balancerNames = getBalancerNames(config);
for (String balancerName : balancerNames)
{
String memberProxyToParam = BALANCER_MEMBER_PREFIX + balancerName + ".ProxyTo";
String proxyTo = config.getInitParameter(memberProxyToParam);
if (proxyTo == null || proxyTo.trim().length() == 0)
{
throw new UnavailableException(memberProxyToParam + " parameter is empty.");
}
_balancerMembers.add(new BalancerMember(balancerName,proxyTo));
}
}
private void initProxyPassReverse(ServletConfig config)
{
_proxyPassReverse = "true".equalsIgnoreCase(config.getInitParameter("ProxyPassReverse"));
}
private void postInit()
{
_roundRobinIterator = new RoundRobinIterator(_balancerMembers);
}
private Set<String> getBalancerNames(ServletConfig config) throws ServletException
{
Set<String> names = new HashSet<String>();
@SuppressWarnings("unchecked")
List<String> initParameterNames = Collections.list(config.getInitParameterNames());
for (String initParameterName : initParameterNames)
{
if (!initParameterName.startsWith(BALANCER_MEMBER_PREFIX))
{
continue;
}
int endOfNameIndex = initParameterName.lastIndexOf(".");
if (endOfNameIndex <= BALANCER_MEMBER_PREFIX.length())
{
throw new UnavailableException(initParameterName + " parameter does not provide a balancer member name");
}
names.add(initParameterName.substring(BALANCER_MEMBER_PREFIX.length(),endOfNameIndex));
}
return names;
}
@Override
protected HttpURI proxyHttpURI(HttpServletRequest request, String uri) throws MalformedURLException
{
BalancerMember balancerMember = selectBalancerMember(request);
try
{
URI dstUri = new URI(balancerMember.getProxyTo() + "/" + uri).normalize();
return new HttpURI(dstUri.toString());
}
catch (URISyntaxException e)
{
throw new MalformedURLException(e.getMessage());
}
}
private BalancerMember selectBalancerMember(HttpServletRequest request)
{
BalancerMember balancerMember = null;
if (_stickySessions)
{
String name = getBalancerMemberNameFromSessionId(request);
if (name != null)
{
balancerMember = findBalancerMemberByName(name);
if (balancerMember != null)
{
return balancerMember;
}
}
}
return _roundRobinIterator.next();
}
private BalancerMember findBalancerMemberByName(String name)
{
BalancerMember example = new BalancerMember(name,"");
for (BalancerMember balancerMember : _balancerMembers)
{
if (balancerMember.equals(example))
{
return balancerMember;
}
}
return null;
}
private String getBalancerMemberNameFromSessionId(HttpServletRequest request)
{
String name = getBalancerMemberNameFromSessionCookie(request);
if (name == null)
{
name = getBalancerMemberNameFromURL(request);
}
return name;
}
private String getBalancerMemberNameFromSessionCookie(HttpServletRequest request)
{
Cookie[] cookies = request.getCookies();
String name = null;
for (Cookie cookie : cookies)
{
if (JSESSIONID.equalsIgnoreCase(cookie.getName()))
{
name = extractBalancerMemberNameFromSessionId(cookie.getValue());
break;
}
}
return name;
}
private String getBalancerMemberNameFromURL(HttpServletRequest request)
{
String name = null;
String requestURI = request.getRequestURI();
int idx = requestURI.lastIndexOf(";");
if (idx != -1)
{
String requestURISuffix = requestURI.substring(idx);
if (requestURISuffix.startsWith(JSESSIONID_URL_PREFIX))
{
name = extractBalancerMemberNameFromSessionId(requestURISuffix.substring(JSESSIONID_URL_PREFIX.length()));
}
}
return name;
}
private String extractBalancerMemberNameFromSessionId(String sessionId)
{
String name = null;
int idx = sessionId.lastIndexOf(".");
if (idx != -1)
{
String sessionIdSuffix = sessionId.substring(idx + 1);
name = (sessionIdSuffix.length() > 0)?sessionIdSuffix:null;
}
return name;
}
@Override
protected String filterResponseHeaderValue(String headerName, String headerValue, HttpServletRequest request)
{
if (_proxyPassReverse && REVERSE_PROXY_HEADERS.contains(headerName))
{
HttpURI locationURI = new HttpURI(headerValue);
if (isAbsoluteLocation(locationURI) && isBackendLocation(locationURI))
{
Request jettyRequest = (Request)request;
URI reverseUri;
try
{
reverseUri = new URI(jettyRequest.getRootURL().append(locationURI.getCompletePath()).toString()).normalize();
return reverseUri.toURL().toString();
}
catch (Exception e)
{
_log.warn("Not filtering header response",e);
return headerValue;
}
}
}
return headerValue;
}
private boolean isBackendLocation(HttpURI locationURI)
{
for (BalancerMember balancerMember : _balancerMembers)
{
HttpURI backendURI = balancerMember.getBackendURI();
if (backendURI.getHost().equals(locationURI.getHost()) && backendURI.getScheme().equals(locationURI.getScheme())
&& backendURI.getPort() == locationURI.getPort())
{
return true;
}
}
return false;
}
private boolean isAbsoluteLocation(HttpURI locationURI)
{
return locationURI.getHost() != null;
}
@Override
public String getHostHeader()
{
throw new UnsupportedOperationException("HostHeader not supported in " + getClass().getName());
}
@Override
public void setHostHeader(String hostHeader)
{
throw new UnsupportedOperationException("HostHeader not supported in " + getClass().getName());
}
@Override
public boolean validateDestination(String host, String path)
{
return true;
}
}

View File

@ -483,13 +483,20 @@ public class ProxyServlet implements Servlet
@Override @Override
protected void onResponseHeader(Buffer name, Buffer value) throws IOException protected void onResponseHeader(Buffer name, Buffer value) throws IOException
{ {
String s = name.toString().toLowerCase(); String nameString = name.toString();
String s = nameString.toLowerCase();
if (!_DontProxyHeaders.contains(s) || (HttpHeaders.CONNECTION_BUFFER.equals(name) && HttpHeaderValues.CLOSE_BUFFER.equals(value))) if (!_DontProxyHeaders.contains(s) || (HttpHeaders.CONNECTION_BUFFER.equals(name) && HttpHeaderValues.CLOSE_BUFFER.equals(value)))
{ {
if (debug != 0) if (debug != 0)
_log.debug(debug + " " + name + ": " + value); _log.debug(debug + " " + name + ": " + value);
response.addHeader(name.toString(),value.toString()); String filteredHeaderValue = filterResponseHeaderValue(nameString,value.toString(),request);
if (filteredHeaderValue != null && filteredHeaderValue.trim().length() > 0)
{
if (debug != 0)
_log.debug(debug + " " + name + ": (filtered): " + filteredHeaderValue);
response.addHeader(nameString,filteredHeaderValue);
}
} }
else if (debug != 0) else if (debug != 0)
_log.debug(debug + " " + name + "! " + value); _log.debug(debug + " " + name + "! " + value);
@ -785,9 +792,23 @@ public class ProxyServlet implements Servlet
} }
} }
/**
* Extension point for remote server response header filtering. The default implementation returns the header value as is. If null is returned, this header
* won't be forwarded back to the client.
*
* @param headerName
* @param headerValue
* @param request
* @return filteredHeaderValue
*/
protected String filterResponseHeaderValue(String headerName, String headerValue, HttpServletRequest request)
{
return headerValue;
}
/** /**
* Transparent Proxy. * Transparent Proxy.
* *
* This convenience extension to ProxyServlet configures the servlet as a transparent proxy. The servlet is configured with init parameters: * This convenience extension to ProxyServlet configures the servlet as a transparent proxy. The servlet is configured with init parameters:
* <ul> * <ul>
* <li>ProxyTo - a URI like http://host:80/context to which the request is proxied. * <li>ProxyTo - a URI like http://host:80/context to which the request is proxied.
@ -795,7 +816,7 @@ public class ProxyServlet implements Servlet
* </ul> * </ul>
* For example, if a request was received at /foo/bar and the ProxyTo was http://host:80/context and the Prefix was /foo, then the request would be proxied * For example, if a request was received at /foo/bar and the ProxyTo was http://host:80/context and the Prefix was /foo, then the request would be proxied
* to http://host:80/context/bar * to http://host:80/context/bar
* *
*/ */
public static class Transparent extends ProxyServlet public static class Transparent extends ProxyServlet
{ {

View File

@ -0,0 +1,157 @@
package org.eclipse.jetty.servlets;
//========================================================================
//Copyright (c) 2012 Intalio, Inc.
//------------------------------------------------------------------------
//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.
//========================================================================
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.server.session.HashSessionIdManager;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.After;
import org.junit.Before;
public abstract class AbstractBalancerServletTest
{
private boolean _stickySessions;
private Server _node1;
private Server _node2;
private Server _balancerServer;
private HttpClient _httpClient;
@Before
public void setUp() throws Exception
{
_httpClient = new HttpClient();
_httpClient.registerListener("org.eclipse.jetty.client.RedirectListener");
_httpClient.start();
}
@After
public void tearDown() throws Exception
{
stopServer(_node1);
stopServer(_node2);
stopServer(_balancerServer);
_httpClient.stop();
}
private void stopServer(Server server)
{
try
{
server.stop();
}
catch (Exception e)
{
// Do nothing
}
}
protected void setStickySessions(boolean stickySessions)
{
_stickySessions = stickySessions;
}
protected void startBalancer(Class<? extends HttpServlet> httpServletClass) throws Exception
{
_node1 = createServer(new ServletHolder(httpServletClass.newInstance()),"/pipo","/molo/*");
setSessionIdManager(_node1,"node1");
_node1.start();
_node2 = createServer(new ServletHolder(httpServletClass.newInstance()),"/pipo","/molo/*");
setSessionIdManager(_node2,"node2");
_node2.start();
BalancerServlet balancerServlet = new BalancerServlet();
ServletHolder balancerServletHolder = new ServletHolder(balancerServlet);
balancerServletHolder.setInitParameter("StickySessions",String.valueOf(_stickySessions));
balancerServletHolder.setInitParameter("ProxyPassReverse","true");
balancerServletHolder.setInitParameter("BalancerMember." + "node1" + ".ProxyTo","http://localhost:" + getServerPort(_node1));
balancerServletHolder.setInitParameter("BalancerMember." + "node2" + ".ProxyTo","http://localhost:" + getServerPort(_node2));
_balancerServer = createServer(balancerServletHolder,"/pipo","/molo/*");
_balancerServer.start();
}
private Server createServer(ServletHolder servletHolder, String appContext, String servletUrlPattern)
{
Server server = new Server();
SelectChannelConnector httpConnector = new SelectChannelConnector();
server.addConnector(httpConnector);
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath(appContext);
server.setHandler(context);
context.addServlet(servletHolder,servletUrlPattern);
return server;
}
private void setSessionIdManager(Server node, String nodeName)
{
HashSessionIdManager sessionIdManager = new HashSessionIdManager();
sessionIdManager.setWorkerName(nodeName);
node.setSessionIdManager(sessionIdManager);
}
private int getServerPort(Server node)
{
return node.getConnectors()[0].getLocalPort();
}
protected byte[] sendRequestToBalancer(String requestUri) throws IOException, InterruptedException
{
ContentExchange exchange = new ContentExchange()
{
@Override
protected void onResponseHeader(Buffer name, Buffer value) throws IOException
{
// Cookie persistence
if (name.toString().equals("Set-Cookie"))
{
String cookieVal = value.toString();
if (cookieVal.startsWith("JSESSIONID="))
{
String jsessionid = cookieVal.split(";")[0].substring("JSESSIONID=".length());
_httpClient.getDestination(getAddress(),false).addCookie(new HttpCookie("JSESSIONID",jsessionid));
}
}
}
};
exchange.setURL("http://localhost:" + getServerPort(_balancerServer) + "/pipo/molo/" + requestUri);
exchange.setMethod(HttpMethods.GET);
_httpClient.send(exchange);
exchange.waitForDone();
return exchange.getResponseContentBytes();
}
}

View File

@ -0,0 +1,129 @@
package org.eclipse.jetty.servlets;
//========================================================================
//Copyright (c) 2012 Intalio, Inc.
//------------------------------------------------------------------------
//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.
//========================================================================
import static org.junit.Assert.*;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Test;
/**
*
*/
public class BalancerServletTest extends AbstractBalancerServletTest
{
@Test
public void testRoundRobinBalancer() throws Exception
{
setStickySessions(false);
startBalancer(CounterServlet.class);
for (int i = 0; i < 10; i++)
{
byte[] responseBytes = sendRequestToBalancer("/");
String returnedCounter = readFirstLine(responseBytes);
// RR : response should increment every other request
String expectedCounter = String.valueOf(i / 2);
assertEquals(expectedCounter,returnedCounter);
}
}
@Test
public void testStickySessionsBalancer() throws Exception
{
setStickySessions(true);
startBalancer(CounterServlet.class);
for (int i = 0; i < 10; i++)
{
byte[] responseBytes = sendRequestToBalancer("/");
String returnedCounter = readFirstLine(responseBytes);
// RR : response should increment on each request
String expectedCounter = String.valueOf(i);
assertEquals(expectedCounter,returnedCounter);
}
}
@Test
public void testProxyPassReverse() throws Exception
{
setStickySessions(false);
startBalancer(RelocationServlet.class);
byte[] responseBytes = sendRequestToBalancer("index.html");
String msg = readFirstLine(responseBytes);
assertEquals("success",msg);
}
private String readFirstLine(byte[] responseBytes) throws IOException
{
BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(responseBytes)));
return reader.readLine();
}
@SuppressWarnings("serial")
public static final class CounterServlet extends HttpServlet
{
private int counter;
@Override
public void init() throws ServletException
{
counter = 0;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
// Force session creation
req.getSession();
resp.setContentType("text/plain");
resp.getWriter().println(counter++);
}
}
@SuppressWarnings("serial")
public static final class RelocationServlet extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
if (req.getRequestURI().endsWith("/index.html"))
{
resp.sendRedirect("http://localhost:" + req.getLocalPort() + req.getContextPath() + req.getServletPath() + "/other.html?secret=pipo%20molo");
return;
}
resp.setContentType("text/plain");
if ("pipo molo".equals(req.getParameter("secret")))
{
resp.getWriter().println("success");
}
else
{
resp.getWriter().println("failure");
}
}
}
}

View File

@ -16,9 +16,11 @@
package org.eclipse.jetty.spdy; package org.eclipse.jetty.spdy;
import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.InterruptedByTimeoutException; import java.nio.channels.InterruptedByTimeoutException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -66,10 +68,12 @@ import org.eclipse.jetty.spdy.frames.WindowUpdateFrame;
import org.eclipse.jetty.spdy.generator.Generator; import org.eclipse.jetty.spdy.generator.Generator;
import org.eclipse.jetty.spdy.parser.Parser; import org.eclipse.jetty.spdy.parser.Parser;
import org.eclipse.jetty.util.Atomics; import org.eclipse.jetty.util.Atomics;
import org.eclipse.jetty.util.component.AggregateLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.Logger;
public class StandardSession implements ISession, Parser.Listener, Handler<StandardSession.FrameBytes> public class StandardSession implements ISession, Parser.Listener, Handler<StandardSession.FrameBytes>, Dumpable
{ {
private static final Logger logger = Log.getLogger(Session.class); private static final Logger logger = Log.getLogger(Session.class);
private static final ThreadLocal<Integer> handlerInvocations = new ThreadLocal<Integer>() private static final ThreadLocal<Integer> handlerInvocations = new ThreadLocal<Integer>()
@ -1092,6 +1096,27 @@ public class StandardSession implements ISession, Parser.Listener, Handler<Stand
flowControlStrategy.setWindowSize(this, initialWindowSize); flowControlStrategy.setWindowSize(this, initialWindowSize);
} }
public String toString()
{
return String.format("%s@%x{v%d,queuSize=%d,windowSize=%d,streams=%d}", getClass().getSimpleName(), hashCode(), version, queue.size(), getWindowSize(), streams.size());
}
@Override
public String dump()
{
return AggregateLifeCycle.dump(this);
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
AggregateLifeCycle.dumpObject(out,this);
AggregateLifeCycle.dump(out,indent,Collections.singletonList(controller),streams.values());
}
public interface FrameBytes extends Comparable<FrameBytes> public interface FrameBytes extends Comparable<FrameBytes>
{ {
public IStream getStream(); public IStream getStream();

View File

@ -440,7 +440,7 @@ public class StandardStream implements IStream
@Override @Override
public String toString() public String toString()
{ {
return String.format("stream=%d v%d %s", getId(), session.getVersion(), closeState); return String.format("stream=%d v%d windowSize=%db reset=%s %s %s", getId(), session.getVersion(), getWindowSize(), isReset(), openState, closeState);
} }
private boolean canSend() private boolean canSend()

View File

@ -11,11 +11,45 @@
<Set name="protocol">TLSv1</Set> <Set name="protocol">TLSv1</Set>
</New> </New>
<!-- Uncomment to create a ReferrerPushStrategy that can be added to the Connectors -->
<!--
<New id="pushStrategy" class="org.eclipse.jetty.spdy.http.ReferrerPushStrategy">
<Arg type="List">
<Array type="String">
<Item>.*\.css</Item>
<Item>.*\.js</Item>
<Item>.*\.png</Item>
<Item>.*\.jpg</Item>
<Item>.*\.gif</Item>
</Array>
</Arg>
</New>
-->
<!--<Set class="org.eclipse.jetty.npn.NextProtoNego" name="debug" type="boolean">true</Set>--> <!--<Set class="org.eclipse.jetty.npn.NextProtoNego" name="debug" type="boolean">true</Set>-->
<Call name="addConnector"> <Call name="addConnector">
<Arg> <Arg>
<New class="org.eclipse.jetty.spdy.http.HTTPSPDYServerConnector"> <New class="org.eclipse.jetty.spdy.http.HTTPSPDYServerConnector">
<!-- uncomment to enable to apply ReferrerPushStrategy for spdy/3
if you want to support it in both spdy/2 and spdy/3, just replace the
value in the first map entry.
-->
<!--
<Arg name="pushStrategies">
<Map>
<Entry>
<Item type="short">2</Item>
<Item><New class="org.eclipse.jetty.spdy.http.PushStrategy$None" /></Item>
</Entry>
<Entry>
<Item type="short">3</Item>
<Item><Ref id="pushStrategy" /></Item>
</Entry>
</Map>
</Arg>
-->
<Set name="Port">8080</Set> <Set name="Port">8080</Set>
</New> </New>
</Arg> </Arg>
@ -26,6 +60,24 @@
<Arg> <Arg>
<Ref id="sslContextFactory" /> <Ref id="sslContextFactory" />
</Arg> </Arg>
<!-- uncomment to enable to apply ReferrerPushStrategy for spdy/3
if you want to support it in both spdy/2 and spdy/3, just replace the
value in the first map entry.
-->
<!--
<Arg name="pushStrategies">
<Map>
<Entry>
<Item type="short">2</Item>
<Item><New class="org.eclipse.jetty.spdy.http.PushStrategy$None" /></Item>
</Entry>
<Entry>
<Item type="short">3</Item>
<Item><Ref id="pushStrategy" /></Item>
</Entry>
</Map>
</Arg>
-->
<Set name="Port">8443</Set> <Set name="Port">8443</Set>
</New> </New>
</Arg> </Arg>

View File

@ -72,6 +72,11 @@
<version>${slf4j-version}</version> <version>${slf4j-version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -16,6 +16,9 @@
package org.eclipse.jetty.spdy.http; package org.eclipse.jetty.spdy.http;
import java.util.Collections;
import java.util.Map;
import org.eclipse.jetty.spdy.api.SPDY; import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
@ -23,32 +26,41 @@ public class HTTPSPDYServerConnector extends AbstractHTTPSPDYServerConnector
{ {
public HTTPSPDYServerConnector() public HTTPSPDYServerConnector()
{ {
this(null, new PushStrategy.None()); this(null, Collections.<Short, PushStrategy>emptyMap());
} }
public HTTPSPDYServerConnector(PushStrategy pushStrategy) public HTTPSPDYServerConnector(Map<Short, PushStrategy> pushStrategies)
{ {
this(null, pushStrategy); this(null, pushStrategies);
} }
public HTTPSPDYServerConnector(SslContextFactory sslContextFactory) public HTTPSPDYServerConnector(SslContextFactory sslContextFactory)
{ {
this(sslContextFactory, new PushStrategy.None()); this(sslContextFactory, Collections.<Short, PushStrategy>emptyMap());
} }
public HTTPSPDYServerConnector(SslContextFactory sslContextFactory, PushStrategy pushStrategy) public HTTPSPDYServerConnector(SslContextFactory sslContextFactory, Map<Short, PushStrategy> pushStrategies)
{ {
// We pass a null ServerSessionFrameListener because for // We pass a null ServerSessionFrameListener because for
// HTTP over SPDY we need one that references the endPoint // HTTP over SPDY we need one that references the endPoint
super(null, sslContextFactory); super(null, sslContextFactory);
clearAsyncConnectionFactories(); clearAsyncConnectionFactories();
// The "spdy/3" protocol handles HTTP over SPDY // The "spdy/3" protocol handles HTTP over SPDY
putAsyncConnectionFactory("spdy/3", new ServerHTTPSPDYAsyncConnectionFactory(SPDY.V3, getByteBufferPool(), getExecutor(), getScheduler(), this, pushStrategy)); putAsyncConnectionFactory("spdy/3", new ServerHTTPSPDYAsyncConnectionFactory(SPDY.V3, getByteBufferPool(), getExecutor(), getScheduler(), this, getPushStrategy(SPDY.V3,pushStrategies)));
// The "spdy/2" protocol handles HTTP over SPDY // The "spdy/2" protocol handles HTTP over SPDY
putAsyncConnectionFactory("spdy/2", new ServerHTTPSPDYAsyncConnectionFactory(SPDY.V2, getByteBufferPool(), getExecutor(), getScheduler(), this, pushStrategy)); putAsyncConnectionFactory("spdy/2", new ServerHTTPSPDYAsyncConnectionFactory(SPDY.V2, getByteBufferPool(), getExecutor(), getScheduler(), this, getPushStrategy(SPDY.V2,pushStrategies)));
// The "http/1.1" protocol handles browsers that support NPN but not SPDY // The "http/1.1" protocol handles browsers that support NPN but not SPDY
putAsyncConnectionFactory("http/1.1", new ServerHTTPAsyncConnectionFactory(this)); putAsyncConnectionFactory("http/1.1", new ServerHTTPAsyncConnectionFactory(this));
// The default connection factory handles plain HTTP on non-SSL or non-NPN connections // The default connection factory handles plain HTTP on non-SSL or non-NPN connections
setDefaultAsyncConnectionFactory(getAsyncConnectionFactory("http/1.1")); setDefaultAsyncConnectionFactory(getAsyncConnectionFactory("http/1.1"));
} }
private PushStrategy getPushStrategy(short version, Map<Short, PushStrategy> pushStrategies)
{
PushStrategy pushStrategy = pushStrategies.get(version);
if(pushStrategy == null)
pushStrategy = new PushStrategy.None();
return pushStrategy;
}
} }

View File

@ -23,6 +23,8 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.eclipse.jetty.spdy.api.Headers; import org.eclipse.jetty.spdy.api.Headers;
@ -37,12 +39,13 @@ import org.eclipse.jetty.util.log.Logger;
* will have a <tt>Referer</tt> HTTP header that points to <tt>index.html</tt>, which we * will have a <tt>Referer</tt> HTTP header that points to <tt>index.html</tt>, which we
* use to link the associated resource to the main resource.</p> * use to link the associated resource to the main resource.</p>
* <p>However, also following a hyperlink generates a HTTP request with a <tt>Referer</tt> * <p>However, also following a hyperlink generates a HTTP request with a <tt>Referer</tt>
* HTTP header that points to <tt>index.html</tt>; therefore main resources and associated * HTTP header that points to <tt>index.html</tt>; therefore a proper value for {@link #getReferrerPushPeriod()}
* resources must be distinguishable.</p> * has to be set. If the referrerPushPeriod for a main resource has been passed, no more
* <p>This class distinguishes associated resources by their URL path suffix and content * associated resources will be added for that main resource.</p>
* <p>This class distinguishes associated main resources by their URL path suffix and content
* type. * type.
* CSS stylesheets, images and JavaScript files have recognizable URL path suffixes that * CSS stylesheets, images and JavaScript files have recognizable URL path suffixes that
* are classified as associated resources.</p> * are classified as associated resources. The suffix regexs can be configured by constructor argument</p>
* <p>When CSS stylesheets refer to images, the CSS image request will have the CSS * <p>When CSS stylesheets refer to images, the CSS image request will have the CSS
* stylesheet as referrer. This implementation will push also the CSS image.</p> * stylesheet as referrer. This implementation will push also the CSS image.</p>
* <p>The push metadata built by this implementation is limited by the number of pages * <p>The push metadata built by this implementation is limited by the number of pages
@ -55,11 +58,12 @@ import org.eclipse.jetty.util.log.Logger;
public class ReferrerPushStrategy implements PushStrategy public class ReferrerPushStrategy implements PushStrategy
{ {
private static final Logger logger = Log.getLogger(ReferrerPushStrategy.class); private static final Logger logger = Log.getLogger(ReferrerPushStrategy.class);
private final ConcurrentMap<String, Set<String>> resources = new ConcurrentHashMap<>(); private final ConcurrentMap<String, MainResource> mainResources = new ConcurrentHashMap<>();
private final Set<Pattern> pushRegexps = new HashSet<>(); private final Set<Pattern> pushRegexps = new HashSet<>();
private final Set<String> pushContentTypes = new HashSet<>(); private final Set<String> pushContentTypes = new HashSet<>();
private final Set<Pattern> allowedPushOrigins = new HashSet<>(); private final Set<Pattern> allowedPushOrigins = new HashSet<>();
private volatile int maxAssociatedResources = 32; private volatile int maxAssociatedResources = 32;
private volatile int referrerPushPeriod = 5000;
public ReferrerPushStrategy() public ReferrerPushStrategy()
{ {
@ -101,22 +105,33 @@ public class ReferrerPushStrategy implements PushStrategy
this.maxAssociatedResources = maxAssociatedResources; this.maxAssociatedResources = maxAssociatedResources;
} }
public int getReferrerPushPeriod()
{
return referrerPushPeriod;
}
public void setReferrerPushPeriod(int referrerPushPeriod)
{
this.referrerPushPeriod = referrerPushPeriod;
}
@Override @Override
public Set<String> apply(Stream stream, Headers requestHeaders, Headers responseHeaders) public Set<String> apply(Stream stream, Headers requestHeaders, Headers responseHeaders)
{ {
Set<String> result = Collections.emptySet(); Set<String> result = Collections.<String>emptySet();
short version = stream.getSession().getVersion(); short version = stream.getSession().getVersion();
String scheme = requestHeaders.get(HTTPSPDYHeader.SCHEME.name(version)).value(); if (!isIfModifiedSinceHeaderPresent(requestHeaders) && isValidMethod(requestHeaders.get(HTTPSPDYHeader.METHOD.name(version)).value()))
String host = requestHeaders.get(HTTPSPDYHeader.HOST.name(version)).value();
String origin = new StringBuilder(scheme).append("://").append(host).toString();
String url = requestHeaders.get(HTTPSPDYHeader.URI.name(version)).value();
String absoluteURL = new StringBuilder(origin).append(url).toString();
logger.debug("Applying push strategy for {}", absoluteURL);
if (isValidMethod(requestHeaders.get(HTTPSPDYHeader.METHOD.name(version)).value()))
{ {
String scheme = requestHeaders.get(HTTPSPDYHeader.SCHEME.name(version)).value();
String host = requestHeaders.get(HTTPSPDYHeader.HOST.name(version)).value();
String origin = scheme + "://" + host;
String url = requestHeaders.get(HTTPSPDYHeader.URI.name(version)).value();
String absoluteURL = origin + url;
logger.debug("Applying push strategy for {}", absoluteURL);
if (isMainResource(url, responseHeaders)) if (isMainResource(url, responseHeaders))
{ {
result = pushResources(absoluteURL); MainResource mainResource = getOrCreateMainResource(absoluteURL);
result = mainResource.getResources();
} }
else if (isPushResource(url, responseHeaders)) else if (isPushResource(url, responseHeaders))
{ {
@ -124,18 +139,49 @@ public class ReferrerPushStrategy implements PushStrategy
if (referrerHeader != null) if (referrerHeader != null)
{ {
String referrer = referrerHeader.value(); String referrer = referrerHeader.value();
Set<String> pushResources = resources.get(referrer); MainResource mainResource = mainResources.get(referrer);
if (pushResources == null || !pushResources.contains(url)) if (mainResource == null)
buildMetadata(origin, url, referrer); mainResource = getOrCreateMainResource(referrer);
Set<String> pushResources = mainResource.getResources();
if (!pushResources.contains(url))
mainResource.addResource(url, origin, referrer);
else else
result = pushResources(absoluteURL); result = getPushResources(absoluteURL);
} }
} }
logger.debug("Pushing {} resources for {}: {}", result.size(), absoluteURL, result);
} }
logger.debug("Push resources for {}: {}", absoluteURL, result);
return result; return result;
} }
private Set<String> getPushResources(String absoluteURL)
{
Set<String> result = Collections.emptySet();
if (mainResources.get(absoluteURL) != null)
result = mainResources.get(absoluteURL).getResources();
return result;
}
private MainResource getOrCreateMainResource(String absoluteURL)
{
MainResource mainResource = mainResources.get(absoluteURL);
if (mainResource == null)
{
logger.debug("Creating new main resource for {}", absoluteURL);
MainResource value = new MainResource(absoluteURL);
mainResource = mainResources.putIfAbsent(absoluteURL, value);
if (mainResource == null)
mainResource = value;
}
return mainResource;
}
private boolean isIfModifiedSinceHeaderPresent(Headers headers)
{
return headers.get("if-modified-since") != null;
}
private boolean isValidMethod(String method) private boolean isValidMethod(String method)
{ {
return "GET".equalsIgnoreCase(method); return "GET".equalsIgnoreCase(method);
@ -165,49 +211,71 @@ public class ReferrerPushStrategy implements PushStrategy
return false; return false;
} }
private Set<String> pushResources(String absoluteURL) private class MainResource
{ {
Set<String> pushResources = resources.get(absoluteURL); private final String name;
if (pushResources == null) private final Set<String> resources = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
return Collections.emptySet(); private final AtomicLong firstResourceAdded = new AtomicLong(-1);
return Collections.unmodifiableSet(pushResources);
}
private void buildMetadata(String origin, String url, String referrer) private MainResource(String name)
{
if (referrer.startsWith(origin) || isPushOriginAllowed(origin))
{ {
Set<String> pushResources = resources.get(referrer); this.name = name;
if (pushResources == null) }
public boolean addResource(String url, String origin, String referrer)
{
// We start the push period here and not when initializing the main resource, because a browser with a
// prefilled cache won't request the subresources. If the browser with warmed up cache now hits the main
// resource after a server restart, the push period shouldn't start until the first subresource is
// being requested.
firstResourceAdded.compareAndSet(-1, System.nanoTime());
long delay = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - firstResourceAdded.get());
if (!referrer.startsWith(origin) && !isPushOriginAllowed(origin))
{ {
pushResources = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); logger.debug("Skipped store of push metadata {} for {}: Origin: {} doesn't match or origin not allowed",
Set<String> existing = resources.putIfAbsent(referrer, pushResources); url, name, origin);
if (existing != null) return false;
pushResources = existing;
} }
// This check is not strictly concurrent-safe, but limiting // This check is not strictly concurrent-safe, but limiting
// the number of associated resources is achieved anyway // the number of associated resources is achieved anyway
// although in rare cases few more resources will be stored // although in rare cases few more resources will be stored
if (pushResources.size() < getMaxAssociatedResources()) if (resources.size() >= maxAssociatedResources)
{
pushResources.add(url);
logger.debug("Stored push metadata for {}: {}", referrer, pushResources);
}
else
{ {
logger.debug("Skipped store of push metadata {} for {}: max associated resources ({}) reached", logger.debug("Skipped store of push metadata {} for {}: max associated resources ({}) reached",
url, referrer, maxAssociatedResources); url, name, maxAssociatedResources);
return false;
}
if (delay > referrerPushPeriod)
{
logger.debug("Delay: {}ms longer than referrerPushPeriod: {}ms. Not adding resource: {} for: {}", delay, referrerPushPeriod, url, name);
return false;
} }
}
}
private boolean isPushOriginAllowed(String origin) logger.debug("Adding resource: {} for: {} with delay: {}ms.", url, name, delay);
{ resources.add(url);
for (Pattern allowedPushOrigin : allowedPushOrigins) return true;
{ }
if (allowedPushOrigin.matcher(origin).matches())
return true; public Set<String> getResources()
{
return Collections.unmodifiableSet(resources);
}
public String toString()
{
return "MainResource: " + name + " associated resources:" + resources.size();
}
private boolean isPushOriginAllowed(String origin)
{
for (Pattern allowedPushOrigin : allowedPushOrigins)
{
if (allowedPushOrigin.matcher(origin).matches())
return true;
}
return false;
} }
return false;
} }
} }

View File

@ -55,6 +55,7 @@ import org.eclipse.jetty.spdy.api.Handler;
import org.eclipse.jetty.spdy.api.Headers; import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.ReplyInfo;
import org.eclipse.jetty.spdy.api.RstInfo; import org.eclipse.jetty.spdy.api.RstInfo;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamStatus; import org.eclipse.jetty.spdy.api.StreamStatus;
import org.eclipse.jetty.spdy.api.SynInfo; import org.eclipse.jetty.spdy.api.SynInfo;
@ -177,6 +178,10 @@ public class ServerHTTPSPDYAsyncConnection extends AbstractHttpConnection implem
logger.debug("HTTP > {} {} {}", m, u, v); logger.debug("HTTP > {} {} {}", m, u, v);
startRequest(new ByteArrayBuffer(m), new ByteArrayBuffer(u), new ByteArrayBuffer(v)); startRequest(new ByteArrayBuffer(m), new ByteArrayBuffer(u), new ByteArrayBuffer(v));
Headers.Header schemeHeader = headers.get(HTTPSPDYHeader.SCHEME.name(this.version));
if(schemeHeader != null)
_request.setScheme(schemeHeader.value());
updateState(State.HEADERS); updateState(State.HEADERS);
handle(); handle();
break; break;
@ -403,7 +408,7 @@ public class ServerHTTPSPDYAsyncConnection extends AbstractHttpConnection implem
if (!stream.isUnidirectional()) if (!stream.isUnidirectional())
stream.reply(replyInfo); stream.reply(replyInfo);
if (replyInfo.getHeaders().get(HTTPSPDYHeader.STATUS.name(version)).value().startsWith("200") && if (replyInfo.getHeaders().get(HTTPSPDYHeader.STATUS.name(version)).value().startsWith("200") &&
!stream.isClosed() && !isIfModifiedSinceHeaderPresent()) !stream.isClosed())
{ {
// We have a 200 OK with some content to send // We have a 200 OK with some content to send
@ -411,19 +416,12 @@ public class ServerHTTPSPDYAsyncConnection extends AbstractHttpConnection implem
Headers.Header host = headers.get(HTTPSPDYHeader.HOST.name(version)); Headers.Header host = headers.get(HTTPSPDYHeader.HOST.name(version));
Headers.Header uri = headers.get(HTTPSPDYHeader.URI.name(version)); Headers.Header uri = headers.get(HTTPSPDYHeader.URI.name(version));
Set<String> pushResources = pushStrategy.apply(stream, headers, replyInfo.getHeaders()); Set<String> pushResources = pushStrategy.apply(stream, headers, replyInfo.getHeaders());
String referrer = new StringBuilder(scheme.value()).append("://").append(host.value()).append(uri.value()).toString();
for (String pushURL : pushResources) for (String pushResourcePath : pushResources)
{ {
final Headers pushHeaders = new Headers(); final Headers requestHeaders = createRequestHeaders(scheme, host, uri, pushResourcePath);
pushHeaders.put(HTTPSPDYHeader.METHOD.name(version), "GET"); final Headers pushHeaders = createPushHeaders(scheme, host, pushResourcePath);
pushHeaders.put(HTTPSPDYHeader.URI.name(version), pushURL);
pushHeaders.put(HTTPSPDYHeader.VERSION.name(version), "HTTP/1.1");
pushHeaders.put(scheme);
pushHeaders.put(host);
pushHeaders.put("referer", referrer);
pushHeaders.put("x-spdy-push", "true");
// Remember support for gzip encoding
pushHeaders.put(headers.get("accept-encoding"));
stream.syn(new SynInfo(pushHeaders, false), getMaxIdleTime(), TimeUnit.MILLISECONDS, new Handler.Adapter<Stream>() stream.syn(new SynInfo(pushHeaders, false), getMaxIdleTime(), TimeUnit.MILLISECONDS, new Handler.Adapter<Stream>()
{ {
@Override @Override
@ -431,16 +429,43 @@ public class ServerHTTPSPDYAsyncConnection extends AbstractHttpConnection implem
{ {
ServerHTTPSPDYAsyncConnection pushConnection = ServerHTTPSPDYAsyncConnection pushConnection =
new ServerHTTPSPDYAsyncConnection(getConnector(), getEndPoint(), getServer(), version, connection, pushStrategy, pushStream); new ServerHTTPSPDYAsyncConnection(getConnector(), getEndPoint(), getServer(), version, connection, pushStrategy, pushStream);
pushConnection.beginRequest(pushHeaders, true); pushConnection.beginRequest(requestHeaders, true);
} }
}); });
} }
} }
} }
private boolean isIfModifiedSinceHeaderPresent() private Headers createRequestHeaders(Headers.Header scheme, Headers.Header host, Headers.Header uri, String pushResourcePath)
{ {
return headers.get("if-modified-since") != null; final Headers requestHeaders = new Headers();
requestHeaders.put(HTTPSPDYHeader.METHOD.name(version), "GET");
requestHeaders.put(HTTPSPDYHeader.VERSION.name(version), "HTTP/1.1");
requestHeaders.put(scheme);
requestHeaders.put(host);
requestHeaders.put(HTTPSPDYHeader.URI.name(version), pushResourcePath);
String referrer = scheme.value() + "://" + host.value() + uri.value();
requestHeaders.put("referer", referrer);
// Remember support for gzip encoding
requestHeaders.put(headers.get("accept-encoding"));
requestHeaders.put("x-spdy-push", "true");
return requestHeaders;
}
private Headers createPushHeaders(Headers.Header scheme, Headers.Header host, String pushResourcePath)
{
final Headers pushHeaders = new Headers();
if (version == SPDY.V2)
pushHeaders.put(HTTPSPDYHeader.URI.name(version), scheme.value() + "://" + host.value() + pushResourcePath);
else
{
pushHeaders.put(HTTPSPDYHeader.URI.name(version), pushResourcePath);
pushHeaders.put(scheme);
pushHeaders.put(host);
}
pushHeaders.put(HTTPSPDYHeader.STATUS.name(version), "200");
pushHeaders.put(HTTPSPDYHeader.VERSION.name(version), "HTTP/1.1");
return pushHeaders;
} }
private Buffer consumeContent(long maxIdleTime) throws IOException, InterruptedException private Buffer consumeContent(long maxIdleTime) throws IOException, InterruptedException

View File

@ -25,6 +25,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import org.eclipse.jetty.spdy.api.Headers; import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener; import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener; import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
@ -71,19 +72,27 @@ public abstract class ProxyEngine extends ServerSessionFrameListener.Adapter imp
return name; return name;
} }
protected void addRequestProxyHeaders(Headers headers) protected void addRequestProxyHeaders(Stream stream, Headers headers)
{ {
String newValue = ""; addViaHeader(headers);
Headers.Header header = headers.get("via");
if (header != null)
newValue = header.valuesAsString() + ", ";
newValue += "http/1.1 " + getName();
headers.put("via", newValue);
} }
protected void addResponseProxyHeaders(Headers headers) protected void addResponseProxyHeaders(Stream stream, Headers headers)
{
addViaHeader(headers);
}
private void addViaHeader(Headers headers)
{
headers.add("Via", "http/1.1 " + getName());
}
protected void customizeRequestHeaders(Stream stream, Headers headers)
{
}
protected void customizeResponseHeaders(Stream stream, Headers headers)
{ {
// TODO: add Via header
} }
public Map<String, ProxyInfo> getProxyInfos() public Map<String, ProxyInfo> getProxyInfos()

View File

@ -130,8 +130,6 @@ public class SPDYProxyEngine extends ProxyEngine
return null; return null;
} }
// TODO: give a chance to modify headers and rewrite URI
short serverVersion = proxyInfo.getVersion(); short serverVersion = proxyInfo.getVersion();
InetSocketAddress address = proxyInfo.getAddress(); InetSocketAddress address = proxyInfo.getAddress();
Session serverSession = produceSession(host, serverVersion, address); Session serverSession = produceSession(host, serverVersion, address);
@ -145,15 +143,13 @@ public class SPDYProxyEngine extends ProxyEngine
Set<Session> sessions = (Set<Session>)serverSession.getAttribute(CLIENT_SESSIONS_ATTRIBUTE); Set<Session> sessions = (Set<Session>)serverSession.getAttribute(CLIENT_SESSIONS_ATTRIBUTE);
sessions.add(clientSession); sessions.add(clientSession);
addRequestProxyHeaders(clientStream, headers);
customizeRequestHeaders(clientStream, headers);
convert(clientVersion, serverVersion, headers); convert(clientVersion, serverVersion, headers);
addRequestProxyHeaders(headers);
SynInfo serverSynInfo = new SynInfo(headers, clientSynInfo.isClose()); SynInfo serverSynInfo = new SynInfo(headers, clientSynInfo.isClose());
logger.debug("P -> S {}", serverSynInfo);
StreamFrameListener listener = new ProxyStreamFrameListener(clientStream); StreamFrameListener listener = new ProxyStreamFrameListener(clientStream);
StreamHandler handler = new StreamHandler(clientStream); StreamHandler handler = new StreamHandler(clientStream, serverSynInfo);
clientStream.setAttribute(STREAM_HANDLER_ATTRIBUTE, handler); clientStream.setAttribute(STREAM_HANDLER_ATTRIBUTE, handler);
serverSession.syn(serverSynInfo, listener, timeout, TimeUnit.MILLISECONDS, handler); serverSession.syn(serverSynInfo, listener, timeout, TimeUnit.MILLISECONDS, handler);
return this; return this;
@ -254,16 +250,19 @@ public class SPDYProxyEngine extends ProxyEngine
@Override @Override
public void onReply(final Stream stream, ReplyInfo replyInfo) public void onReply(final Stream stream, ReplyInfo replyInfo)
{ {
logger.debug("S -> P {} on {}", replyInfo, stream);
short serverVersion = stream.getSession().getVersion(); short serverVersion = stream.getSession().getVersion();
Headers headers = new Headers(replyInfo.getHeaders(), false); Headers headers = new Headers(replyInfo.getHeaders(), false);
addResponseProxyHeaders(stream, headers);
customizeResponseHeaders(stream, headers);
short clientVersion = this.clientStream.getSession().getVersion(); short clientVersion = this.clientStream.getSession().getVersion();
convert(serverVersion, clientVersion, headers); convert(serverVersion, clientVersion, headers);
addResponseProxyHeaders(headers);
this.replyInfo = new ReplyInfo(headers, replyInfo.isClose()); this.replyInfo = new ReplyInfo(headers, replyInfo.isClose());
if (replyInfo.isClose()) if (replyInfo.isClose())
reply(); reply(stream);
} }
@Override @Override
@ -276,19 +275,29 @@ public class SPDYProxyEngine extends ProxyEngine
@Override @Override
public void onData(final Stream stream, final DataInfo dataInfo) public void onData(final Stream stream, final DataInfo dataInfo)
{ {
logger.debug("S -> P {} on {}", dataInfo, stream);
if (replyInfo != null) if (replyInfo != null)
{ {
if (dataInfo.isClose()) if (dataInfo.isClose())
replyInfo.getHeaders().put("content-length", String.valueOf(dataInfo.available())); replyInfo.getHeaders().put("content-length", String.valueOf(dataInfo.available()));
reply(); reply(stream);
} }
data(dataInfo); data(stream, dataInfo);
} }
private void reply() private void reply(final Stream stream)
{ {
clientStream.reply(replyInfo, getTimeout(), TimeUnit.MILLISECONDS, new Handler.Adapter<Void>() final ReplyInfo replyInfo = this.replyInfo;
this.replyInfo = null;
clientStream.reply(replyInfo, getTimeout(), TimeUnit.MILLISECONDS, new Handler<Void>()
{ {
@Override
public void completed(Void context)
{
logger.debug("P -> C {} from {} to {}", replyInfo, stream, clientStream);
}
@Override @Override
public void failed(Void context, Throwable x) public void failed(Void context, Throwable x)
{ {
@ -296,10 +305,9 @@ public class SPDYProxyEngine extends ProxyEngine
rst(clientStream); rst(clientStream);
} }
}); });
replyInfo = null;
} }
private void data(final DataInfo dataInfo) private void data(final Stream stream, final DataInfo dataInfo)
{ {
clientStream.data(dataInfo, getTimeout(), TimeUnit.MILLISECONDS, new Handler<Void>() clientStream.data(dataInfo, getTimeout(), TimeUnit.MILLISECONDS, new Handler<Void>()
{ {
@ -307,6 +315,7 @@ public class SPDYProxyEngine extends ProxyEngine
public void completed(Void context) public void completed(Void context)
{ {
dataInfo.consume(dataInfo.length()); dataInfo.consume(dataInfo.length());
logger.debug("P -> C {} from {} to {}", dataInfo, stream, clientStream);
} }
@Override @Override
@ -331,16 +340,20 @@ public class SPDYProxyEngine extends ProxyEngine
{ {
private final Queue<DataInfoHandler> queue = new LinkedList<>(); private final Queue<DataInfoHandler> queue = new LinkedList<>();
private final Stream clientStream; private final Stream clientStream;
private final SynInfo serverSynInfo;
private Stream serverStream; private Stream serverStream;
private StreamHandler(Stream clientStream) private StreamHandler(Stream clientStream, SynInfo serverSynInfo)
{ {
this.clientStream = clientStream; this.clientStream = clientStream;
this.serverSynInfo = serverSynInfo;
} }
@Override @Override
public void completed(Stream serverStream) public void completed(Stream serverStream)
{ {
logger.debug("P -> S {} from {} to {}", serverSynInfo, clientStream, serverStream);
serverStream.setAttribute(CLIENT_STREAM_ATTRIBUTE, clientStream); serverStream.setAttribute(CLIENT_STREAM_ATTRIBUTE, clientStream);
DataInfoHandler dataInfoHandler; DataInfoHandler dataInfoHandler;
@ -470,14 +483,15 @@ public class SPDYProxyEngine extends ProxyEngine
Headers headers = new Headers(serverSynInfo.getHeaders(), false); Headers headers = new Headers(serverSynInfo.getHeaders(), false);
addResponseProxyHeaders(serverStream, headers);
customizeResponseHeaders(serverStream, headers);
Stream clientStream = (Stream)serverStream.getAssociatedStream().getAttribute(CLIENT_STREAM_ATTRIBUTE); Stream clientStream = (Stream)serverStream.getAssociatedStream().getAttribute(CLIENT_STREAM_ATTRIBUTE);
convert(serverStream.getSession().getVersion(), clientStream.getSession().getVersion(), headers); convert(serverStream.getSession().getVersion(), clientStream.getSession().getVersion(), headers);
addResponseProxyHeaders(headers); StreamHandler handler = new StreamHandler(clientStream, serverSynInfo);
StreamHandler handler = new StreamHandler(clientStream);
serverStream.setAttribute(STREAM_HANDLER_ATTRIBUTE, handler); serverStream.setAttribute(STREAM_HANDLER_ATTRIBUTE, handler);
clientStream.syn(new SynInfo(headers, serverSynInfo.isClose()), getTimeout(), TimeUnit.MILLISECONDS, handler); clientStream.syn(new SynInfo(headers, serverSynInfo.isClose()), getTimeout(), TimeUnit.MILLISECONDS, handler);
return this; return this;
} }

View File

@ -137,39 +137,21 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest
for (int i = 0; i < cssResources.length; ++i) for (int i = 0; i < cssResources.length; ++i)
{ {
String path = "/" + i + ".css"; String path = "/" + i + ".css";
exchange = new TestExchange(); exchange = createExchangeWithReferrer(referrer, path);
exchange.setMethod("GET");
exchange.setRequestURI(path);
exchange.setVersion("HTTP/1.1");
exchange.setAddress(new Address("localhost", connector.getLocalPort()));
exchange.setRequestHeader("Host", "localhost:" + connector.getLocalPort());
exchange.setRequestHeader("referer", referrer);
++result; ++result;
httpClient.send(exchange); httpClient.send(exchange);
} }
for (int i = 0; i < jsResources.length; ++i) for (int i = 0; i < jsResources.length; ++i)
{ {
String path = "/" + i + ".js"; String path = "/" + i + ".js";
exchange = new TestExchange(); exchange = createExchangeWithReferrer(referrer, path);
exchange.setMethod("GET");
exchange.setRequestURI(path);
exchange.setVersion("HTTP/1.1");
exchange.setAddress(new Address("localhost", connector.getLocalPort()));
exchange.setRequestHeader("Host", "localhost:" + connector.getLocalPort());
exchange.setRequestHeader("referer", referrer);
++result; ++result;
httpClient.send(exchange); httpClient.send(exchange);
} }
for (int i = 0; i < pngResources.length; ++i) for (int i = 0; i < pngResources.length; ++i)
{ {
String path = "/" + i + ".png"; String path = "/" + i + ".png";
exchange = new TestExchange(); exchange = createExchangeWithReferrer(referrer, path);
exchange.setMethod("GET");
exchange.setRequestURI(path);
exchange.setVersion("HTTP/1.1");
exchange.setAddress(new Address("localhost", connector.getLocalPort()));
exchange.setRequestHeader("Host", "localhost:" + connector.getLocalPort());
exchange.setRequestHeader("referer", referrer);
++result; ++result;
httpClient.send(exchange); httpClient.send(exchange);
} }
@ -180,6 +162,19 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest
return result; return result;
} }
private ContentExchange createExchangeWithReferrer(String referrer, String path)
{
ContentExchange exchange;
exchange = new TestExchange();
exchange.setMethod("GET");
exchange.setRequestURI(path);
exchange.setVersion("HTTP/1.1");
exchange.setAddress(new Address("localhost", connector.getLocalPort()));
exchange.setRequestHeader("Host", "localhost:" + connector.getLocalPort());
exchange.setRequestHeader("referer", referrer);
return exchange;
}
private void benchmarkSPDY(PushStrategy pushStrategy, Session session) throws Exception private void benchmarkSPDY(PushStrategy pushStrategy, Session session) throws Exception
{ {
@ -238,13 +233,7 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest
String path = "/" + i + ".css"; String path = "/" + i + ".css";
if (pushedResources.contains(path)) if (pushedResources.contains(path))
continue; continue;
headers = new Headers(); headers = createRequestHeaders(referrer, path);
headers.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
headers.put(HTTPSPDYHeader.URI.name(version()), path);
headers.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
headers.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
headers.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
headers.put("referer", referrer);
++result; ++result;
session.syn(new SynInfo(headers, true), new DataListener()); session.syn(new SynInfo(headers, true), new DataListener());
} }
@ -253,13 +242,7 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest
String path = "/" + i + ".js"; String path = "/" + i + ".js";
if (pushedResources.contains(path)) if (pushedResources.contains(path))
continue; continue;
headers = new Headers(); headers = createRequestHeaders(referrer, path);
headers.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
headers.put(HTTPSPDYHeader.URI.name(version()), path);
headers.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
headers.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
headers.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
headers.put("referer", referrer);
++result; ++result;
session.syn(new SynInfo(headers, true), new DataListener()); session.syn(new SynInfo(headers, true), new DataListener());
} }
@ -268,13 +251,7 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest
String path = "/" + i + ".png"; String path = "/" + i + ".png";
if (pushedResources.contains(path)) if (pushedResources.contains(path))
continue; continue;
headers = new Headers(); headers = createRequestHeaders(referrer, path);
headers.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
headers.put(HTTPSPDYHeader.URI.name(version()), path);
headers.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
headers.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
headers.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
headers.put("referer", referrer);
++result; ++result;
session.syn(new SynInfo(headers, true), new DataListener()); session.syn(new SynInfo(headers, true), new DataListener());
} }
@ -285,6 +262,19 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest
return result; return result;
} }
private Headers createRequestHeaders(String referrer, String path)
{
Headers headers;
headers = new Headers();
headers.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
headers.put(HTTPSPDYHeader.URI.name(version()), path);
headers.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
headers.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
headers.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
headers.put("referer", referrer);
return headers;
}
private void sleep(long delay) throws ServletException private void sleep(long delay) throws ServletException
{ {
try try

View File

@ -0,0 +1,106 @@
package org.eclipse.jetty.spdy.http;
import java.util.Set;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.Stream;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class ReferrerPushStrategyUnitTest
{
public static final short VERSION = SPDY.V3;
public static final String SCHEME = "http";
public static final String HOST = "localhost";
public static final String MAIN_URI = "/index.html";
public static final String METHOD = "GET";
// class under test
private ReferrerPushStrategy referrerPushStrategy;
@Mock
Stream stream;
@Mock
Session session;
@Before
public void setup()
{
referrerPushStrategy = new ReferrerPushStrategy();
}
@Test
public void testReferrerCallsAfterTimeoutAreNotAddedAsPushResources() throws InterruptedException
{
Headers requestHeaders = getBaseHeaders(VERSION);
int referrerCallTimeout = 1000;
referrerPushStrategy.setReferrerPushPeriod(referrerCallTimeout);
setMockExpectations();
String referrerUrl = fillPushStrategyCache(requestHeaders);
Set<String> pushResources;
// sleep to pretend that the user manually clicked on a linked resource instead the browser requesting subresources immediately
Thread.sleep(referrerCallTimeout + 1);
requestHeaders.put(HTTPSPDYHeader.URI.name(VERSION), "image2.jpg");
requestHeaders.put("referer", referrerUrl);
pushResources = referrerPushStrategy.apply(stream, requestHeaders, new Headers());
assertThat("pushResources is empty", pushResources.size(), is(0));
requestHeaders.put(HTTPSPDYHeader.URI.name(VERSION), MAIN_URI);
pushResources = referrerPushStrategy.apply(stream, requestHeaders, new Headers());
// as the image2.jpg request has been a link and not a subresource, we expect that pushResources.size() is still 2
assertThat("pushResources contains two elements image.jpg and style.css", pushResources.size(), is(2));
}
private Headers getBaseHeaders(short version)
{
Headers requestHeaders = new Headers();
requestHeaders.put(HTTPSPDYHeader.SCHEME.name(version), SCHEME);
requestHeaders.put(HTTPSPDYHeader.HOST.name(version), HOST);
requestHeaders.put(HTTPSPDYHeader.URI.name(version), MAIN_URI);
requestHeaders.put(HTTPSPDYHeader.METHOD.name(version), METHOD);
return requestHeaders;
}
private void setMockExpectations()
{
when(stream.getSession()).thenReturn(session);
when(session.getVersion()).thenReturn(VERSION);
}
private String fillPushStrategyCache(Headers requestHeaders)
{
Set<String> pushResources = referrerPushStrategy.apply(stream, requestHeaders, new Headers());
assertThat("pushResources is empty", pushResources.size(), is(0));
String origin = SCHEME + "://" + HOST;
String referrerUrl = origin + MAIN_URI;
requestHeaders.put(HTTPSPDYHeader.URI.name(VERSION), "image.jpg");
requestHeaders.put("referer", referrerUrl);
pushResources = referrerPushStrategy.apply(stream, requestHeaders, new Headers());
assertThat("pushResources is empty", pushResources.size(), is(0));
requestHeaders.put(HTTPSPDYHeader.URI.name(VERSION), "style.css");
pushResources = referrerPushStrategy.apply(stream, requestHeaders, new Headers());
assertThat("pushResources is empty", pushResources.size(), is(0));
requestHeaders.put(HTTPSPDYHeader.URI.name(VERSION), MAIN_URI);
pushResources = referrerPushStrategy.apply(stream, requestHeaders, new Headers());
assertThat("pushResources contains two elements image.jpg and style.css", pushResources.size(), is(2));
return referrerUrl;
}
}

View File

@ -7,7 +7,7 @@
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
@ -32,6 +32,7 @@ import org.eclipse.jetty.spdy.SPDYServerConnector;
import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.api.Headers; import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.ReplyInfo;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.SessionFrameListener; import org.eclipse.jetty.spdy.api.SessionFrameListener;
import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.Stream;
@ -42,6 +43,10 @@ import org.junit.Test;
public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
{ {
private final String mainResource = "/index.html";
private final String cssResource = "/style.css";
@Override @Override
protected SPDYServerConnector newHTTPSPDYServerConnector(short version) protected SPDYServerConnector newHTTPSPDYServerConnector(short version)
{ {
@ -51,10 +56,71 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
return connector; return connector;
} }
@Test
public void testPushHeadersAreValid() throws Exception
{
InetSocketAddress address = createServer();
ReferrerPushStrategy pushStrategy = new ReferrerPushStrategy();
int referrerPushPeriod = 1000;
pushStrategy.setReferrerPushPeriod(referrerPushPeriod);
AsyncConnectionFactory defaultFactory = new ServerHTTPSPDYAsyncConnectionFactory(version(), connector.getByteBufferPool(), connector.getExecutor(), connector.getScheduler(), connector, pushStrategy);
connector.setDefaultAsyncConnectionFactory(defaultFactory);
Headers mainRequestHeaders = createHeadersWithoutReferrer(mainResource);
Session session1 = sendMainRequestAndCSSRequest(address, mainRequestHeaders);
// Sleep for pushPeriod This should prevent application.js from being mapped as pushResource
Thread.sleep(referrerPushPeriod + 1);
sendJSRequest(session1);
run2ndClientRequests(address, mainRequestHeaders, true);
}
@Test
public void testReferrerPushPeriod() throws Exception
{
InetSocketAddress address = createServer();
ReferrerPushStrategy pushStrategy = new ReferrerPushStrategy();
int referrerPushPeriod = 1000;
pushStrategy.setReferrerPushPeriod(referrerPushPeriod);
AsyncConnectionFactory defaultFactory = new ServerHTTPSPDYAsyncConnectionFactory(version(), connector.getByteBufferPool(), connector.getExecutor(), connector.getScheduler(), connector, pushStrategy);
connector.setDefaultAsyncConnectionFactory(defaultFactory);
Headers mainRequestHeaders = createHeadersWithoutReferrer(mainResource);
Session session1 = sendMainRequestAndCSSRequest(address, mainRequestHeaders);
// Sleep for pushPeriod This should prevent application.js from being mapped as pushResource
Thread.sleep(referrerPushPeriod+1);
sendJSRequest(session1);
run2ndClientRequests(address, mainRequestHeaders, false);
}
@Test @Test
public void testMaxAssociatedResources() throws Exception public void testMaxAssociatedResources() throws Exception
{ {
InetSocketAddress address = startHTTPServer(version(), new AbstractHandler() InetSocketAddress address = createServer();
ReferrerPushStrategy pushStrategy = new ReferrerPushStrategy();
pushStrategy.setMaxAssociatedResources(1);
AsyncConnectionFactory defaultFactory = new ServerHTTPSPDYAsyncConnectionFactory(version(), connector.getByteBufferPool(), connector.getExecutor(), connector.getScheduler(), connector, pushStrategy);
connector.setDefaultAsyncConnectionFactory(defaultFactory);
Headers mainRequestHeaders = createHeadersWithoutReferrer(mainResource);
Session session1 = sendMainRequestAndCSSRequest(address, mainRequestHeaders);
sendJSRequest(session1);
run2ndClientRequests(address, mainRequestHeaders, false);
}
private InetSocketAddress createServer() throws Exception
{
return startHTTPServer(version(), new AbstractHandler()
{ {
@Override @Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
@ -70,21 +136,13 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
baseRequest.setHandled(true); baseRequest.setHandled(true);
} }
}); });
ReferrerPushStrategy pushStrategy = new ReferrerPushStrategy(); }
pushStrategy.setMaxAssociatedResources(1);
AsyncConnectionFactory defaultFactory = new ServerHTTPSPDYAsyncConnectionFactory(version(), connector.getByteBufferPool(), connector.getExecutor(), connector.getScheduler(), connector, pushStrategy);
connector.setDefaultAsyncConnectionFactory(defaultFactory);
private Session sendMainRequestAndCSSRequest(InetSocketAddress address, Headers mainRequestHeaders) throws Exception
{
Session session1 = startClient(version(), address, null); Session session1 = startClient(version(), address, null);
final CountDownLatch mainResourceLatch = new CountDownLatch(1); final CountDownLatch mainResourceLatch = new CountDownLatch(1);
Headers mainRequestHeaders = new Headers();
mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
String mainResource = "/index.html";
mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource);
mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -98,13 +156,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS));
final CountDownLatch associatedResourceLatch1 = new CountDownLatch(1); final CountDownLatch associatedResourceLatch1 = new CountDownLatch(1);
Headers associatedRequestHeaders1 = new Headers(); Headers associatedRequestHeaders1 = createHeaders(cssResource);
associatedRequestHeaders1.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
associatedRequestHeaders1.put(HTTPSPDYHeader.URI.name(version()), "/style.css");
associatedRequestHeaders1.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
associatedRequestHeaders1.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
associatedRequestHeaders1.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
associatedRequestHeaders1.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource);
session1.syn(new SynInfo(associatedRequestHeaders1, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(associatedRequestHeaders1, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -116,15 +168,15 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
} }
}); });
Assert.assertTrue(associatedResourceLatch1.await(5, TimeUnit.SECONDS)); Assert.assertTrue(associatedResourceLatch1.await(5, TimeUnit.SECONDS));
return session1;
}
private void sendJSRequest(Session session1) throws InterruptedException
{
final CountDownLatch associatedResourceLatch2 = new CountDownLatch(1); final CountDownLatch associatedResourceLatch2 = new CountDownLatch(1);
Headers associatedRequestHeaders2 = new Headers(); String jsResource = "/application.js";
associatedRequestHeaders2.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); Headers associatedRequestHeaders2 = createHeaders(jsResource);
associatedRequestHeaders2.put(HTTPSPDYHeader.URI.name(version()), "/application.js");
associatedRequestHeaders2.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
associatedRequestHeaders2.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
associatedRequestHeaders2.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
associatedRequestHeaders2.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource);
session1.syn(new SynInfo(associatedRequestHeaders2, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(associatedRequestHeaders2, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -136,17 +188,24 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
} }
}); });
Assert.assertTrue(associatedResourceLatch2.await(5, TimeUnit.SECONDS)); Assert.assertTrue(associatedResourceLatch2.await(5, TimeUnit.SECONDS));
}
private void run2ndClientRequests(InetSocketAddress address, Headers mainRequestHeaders, final boolean validateHeaders) throws Exception
{
// Create another client, and perform the same request for the main resource, // Create another client, and perform the same request for the main resource,
// we expect the css being pushed, but not the js // we expect the css being pushed, but not the js
final CountDownLatch mainStreamLatch = new CountDownLatch(2); final CountDownLatch mainStreamLatch = new CountDownLatch(2);
final CountDownLatch pushDataLatch = new CountDownLatch(1); final CountDownLatch pushDataLatch = new CountDownLatch(1);
final CountDownLatch pushSynHeadersValid = new CountDownLatch(1);
Session session2 = startClient(version(), address, new SessionFrameListener.Adapter() Session session2 = startClient(version(), address, new SessionFrameListener.Adapter()
{ {
@Override @Override
public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) public StreamFrameListener onSyn(Stream stream, SynInfo synInfo)
{ {
if(validateHeaders)
validateHeaders(synInfo.getHeaders(), pushSynHeadersValid);
Assert.assertTrue(stream.isUnidirectional()); Assert.assertTrue(stream.isUnidirectional());
Assert.assertTrue(synInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version())).value().endsWith(".css")); Assert.assertTrue(synInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version())).value().endsWith(".css"));
return new StreamFrameListener.Adapter() return new StreamFrameListener.Adapter()
@ -180,8 +239,10 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
} }
}); });
Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue("Main request reply and/or data not received", mainStreamLatch.await(5, TimeUnit.SECONDS));
Assert.assertTrue(pushDataLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue("Pushed data not received", pushDataLatch.await(5, TimeUnit.SECONDS));
if(validateHeaders)
Assert.assertTrue("Push syn headers not valid", pushSynHeadersValid.await(5, TimeUnit.SECONDS));
} }
@Test @Test
@ -204,13 +265,8 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Session session1 = startClient(version(), address, null); Session session1 = startClient(version(), address, null);
final CountDownLatch mainResourceLatch = new CountDownLatch(1); final CountDownLatch mainResourceLatch = new CountDownLatch(1);
Headers mainRequestHeaders = new Headers(); Headers mainRequestHeaders = createHeadersWithoutReferrer(mainResource);
mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
String mainResource = "/index.html";
mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource);
mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -224,13 +280,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS));
final CountDownLatch associatedResourceLatch = new CountDownLatch(1); final CountDownLatch associatedResourceLatch = new CountDownLatch(1);
Headers associatedRequestHeaders = new Headers(); Headers associatedRequestHeaders = createHeaders(cssResource);
associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
associatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/style.css");
associatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
associatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
associatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource);
session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -290,6 +340,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
@Test @Test
public void testAssociatedResourceWithWrongContentTypeIsNotPushed() throws Exception public void testAssociatedResourceWithWrongContentTypeIsNotPushed() throws Exception
{ {
final String fakeResource = "/fake.png";
InetSocketAddress address = startHTTPServer(version(), new AbstractHandler() InetSocketAddress address = startHTTPServer(version(), new AbstractHandler()
{ {
@Override @Override
@ -302,7 +353,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
response.setContentType("text/html"); response.setContentType("text/html");
output.print("<html><head/><body>HELLO</body></html>"); output.print("<html><head/><body>HELLO</body></html>");
} }
else if (url.equals("/fake.png")) else if (url.equals(fakeResource))
{ {
response.setContentType("text/html"); response.setContentType("text/html");
output.print("<html><head/><body>IMAGE</body></html>"); output.print("<html><head/><body>IMAGE</body></html>");
@ -318,13 +369,8 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Session session1 = startClient(version(), address, null); Session session1 = startClient(version(), address, null);
final CountDownLatch mainResourceLatch = new CountDownLatch(1); final CountDownLatch mainResourceLatch = new CountDownLatch(1);
Headers mainRequestHeaders = new Headers(); Headers mainRequestHeaders = createHeadersWithoutReferrer(mainResource);
mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
String mainResource = "/index.html";
mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource);
mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -338,13 +384,8 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS));
final CountDownLatch associatedResourceLatch = new CountDownLatch(1); final CountDownLatch associatedResourceLatch = new CountDownLatch(1);
Headers associatedRequestHeaders = new Headers(); String cssResource = "/stylesheet.css";
associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); Headers associatedRequestHeaders = createHeaders(cssResource);
associatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/stylesheet.css");
associatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
associatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
associatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource);
session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -358,13 +399,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS));
final CountDownLatch fakeAssociatedResourceLatch = new CountDownLatch(1); final CountDownLatch fakeAssociatedResourceLatch = new CountDownLatch(1);
Headers fakeAssociatedRequestHeaders = new Headers(); Headers fakeAssociatedRequestHeaders = createHeaders(fakeResource);
fakeAssociatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
fakeAssociatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/fake.png");
fakeAssociatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
fakeAssociatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
fakeAssociatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
fakeAssociatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource);
session1.syn(new SynInfo(fakeAssociatedRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(fakeAssociatedRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -445,13 +480,8 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Session session1 = startClient(version(), address, null); Session session1 = startClient(version(), address, null);
final CountDownLatch mainResourceLatch = new CountDownLatch(1); final CountDownLatch mainResourceLatch = new CountDownLatch(1);
Headers mainRequestHeaders = new Headers(); Headers mainRequestHeaders = createHeadersWithoutReferrer(mainResource);
mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
String mainResource = "/index.html";
mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource);
mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -465,14 +495,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS));
final CountDownLatch associatedResourceLatch = new CountDownLatch(1); final CountDownLatch associatedResourceLatch = new CountDownLatch(1);
Headers associatedRequestHeaders = new Headers(); Headers associatedRequestHeaders = createHeaders(cssResource);
associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
String associatedResource = "/style.css";
associatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), associatedResource);
associatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
associatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
associatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource);
session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -486,13 +509,9 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS));
final CountDownLatch nestedResourceLatch = new CountDownLatch(1); final CountDownLatch nestedResourceLatch = new CountDownLatch(1);
Headers nestedRequestHeaders = new Headers(); String imageUrl = "/image.gif";
nestedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); Headers nestedRequestHeaders = createHeaders(imageUrl, cssResource);
nestedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/image.gif");
nestedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
nestedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
nestedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
nestedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + associatedResource);
session1.syn(new SynInfo(nestedRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(nestedRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -567,13 +586,8 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Session session1 = startClient(version(), address, null); Session session1 = startClient(version(), address, null);
final CountDownLatch mainResourceLatch = new CountDownLatch(1); final CountDownLatch mainResourceLatch = new CountDownLatch(1);
Headers mainRequestHeaders = new Headers(); Headers mainRequestHeaders = createHeadersWithoutReferrer(mainResource);
mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
String mainResource = "/index.html";
mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource);
mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -587,13 +601,9 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS));
final CountDownLatch associatedResourceLatch = new CountDownLatch(1); final CountDownLatch associatedResourceLatch = new CountDownLatch(1);
Headers associatedRequestHeaders = new Headers(); String associatedResource = "/home.html";
associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); Headers associatedRequestHeaders = createHeaders(associatedResource);
associatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/home.html");
associatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
associatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
associatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource);
session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -661,13 +671,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Session session1 = startClient(version(), address, null); Session session1 = startClient(version(), address, null);
final CountDownLatch mainResourceLatch = new CountDownLatch(1); final CountDownLatch mainResourceLatch = new CountDownLatch(1);
Headers mainRequestHeaders = new Headers(); Headers mainRequestHeaders = createHeaders(mainResource);
mainRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
String mainResource = "/index.html";
mainRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), mainResource);
mainRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
mainRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
mainRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
mainRequestHeaders.put("If-Modified-Since", "Tue, 27 Mar 2012 16:36:52 GMT"); mainRequestHeaders.put("If-Modified-Since", "Tue, 27 Mar 2012 16:36:52 GMT");
session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@ -682,13 +686,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS));
final CountDownLatch associatedResourceLatch = new CountDownLatch(1); final CountDownLatch associatedResourceLatch = new CountDownLatch(1);
Headers associatedRequestHeaders = new Headers(); Headers associatedRequestHeaders = createHeaders(cssResource);
associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
associatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), "/style.css");
associatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
associatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
associatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + mainResource);
session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter()
{ {
@Override @Override
@ -745,4 +743,57 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest
Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS));
Assert.assertFalse("We don't expect data to be pushed as the main request contained an if-modified-since header",pushDataLatch.await(1, TimeUnit.SECONDS)); Assert.assertFalse("We don't expect data to be pushed as the main request contained an if-modified-since header",pushDataLatch.await(1, TimeUnit.SECONDS));
} }
private void validateHeaders(Headers headers, CountDownLatch pushSynHeadersValid)
{
if (validateHeader(headers, HTTPSPDYHeader.STATUS.name(version()), "200")
&& validateHeader(headers, HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1")
&& validateUriHeader(headers))
pushSynHeadersValid.countDown();
}
private boolean validateHeader(Headers headers, String name, String expectedValue)
{
Headers.Header header = headers.get(name);
if (header != null && expectedValue.equals(header.value()))
return true;
System.out.println(name + " not valid! " + headers);
return false;
}
private boolean validateUriHeader(Headers headers)
{
Headers.Header uriHeader = headers.get(HTTPSPDYHeader.URI.name(version()));
if (uriHeader != null)
if (version() == SPDY.V2 && uriHeader.value().startsWith("http://"))
return true;
else if (version() == SPDY.V3 && uriHeader.value().startsWith("/")
&& headers.get(HTTPSPDYHeader.HOST.name(version())) != null && headers.get(HTTPSPDYHeader.SCHEME.name(version())) != null)
return true;
System.out.println(HTTPSPDYHeader.URI.name(version()) + " not valid!");
return false;
}
private Headers createHeaders(String resource)
{
return createHeaders(resource, mainResource);
}
private Headers createHeaders(String resource, String referrer)
{
Headers associatedRequestHeaders = createHeadersWithoutReferrer(resource);
associatedRequestHeaders.put("referer", "http://localhost:" + connector.getLocalPort() + referrer);
return associatedRequestHeaders;
}
private Headers createHeadersWithoutReferrer(String resource)
{
Headers associatedRequestHeaders = new Headers();
associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET");
associatedRequestHeaders.put(HTTPSPDYHeader.URI.name(version()), resource);
associatedRequestHeaders.put(HTTPSPDYHeader.VERSION.name(version()), "HTTP/1.1");
associatedRequestHeaders.put(HTTPSPDYHeader.SCHEME.name(version()), "http");
associatedRequestHeaders.put(HTTPSPDYHeader.HOST.name(version()), "localhost:" + connector.getLocalPort());
return associatedRequestHeaders;
}
} }

View File

@ -0,0 +1,81 @@
package org.eclipse.jetty.spdy.http;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jetty.spdy.SPDYClient;
import org.eclipse.jetty.spdy.api.Headers;
import org.eclipse.jetty.spdy.api.ReplyInfo;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
public class SSLExternalServerTest extends AbstractHTTPSPDYTest
{
@Override
protected SPDYClient.Factory newSPDYClientFactory(Executor threadPool)
{
SslContextFactory sslContextFactory = new SslContextFactory();
// Force TLSv1
sslContextFactory.setIncludeProtocols("TLSv1");
return new SPDYClient.Factory(threadPool, sslContextFactory);
}
@Test
public void testExternalServer() throws Exception
{
String host = "encrypted.google.com";
int port = 443;
InetSocketAddress address = new InetSocketAddress(host, port);
try
{
// Test whether there is connectivity to avoid fail the test when offline
Socket socket = new Socket();
socket.connect(address, 5000);
socket.close();
}
catch (IOException x)
{
Assume.assumeNoException(x);
}
final short version = SPDY.V2;
Session session = startClient(version, address, null);
Headers headers = new Headers();
headers.put(HTTPSPDYHeader.SCHEME.name(version), "https");
headers.put(HTTPSPDYHeader.HOST.name(version), host + ":" + port);
headers.put(HTTPSPDYHeader.METHOD.name(version), "GET");
headers.put(HTTPSPDYHeader.URI.name(version), "/");
headers.put(HTTPSPDYHeader.VERSION.name(version), "HTTP/1.1");
final CountDownLatch latch = new CountDownLatch(1);
session.syn(new SynInfo(headers, true), new StreamFrameListener.Adapter()
{
@Override
public void onReply(Stream stream, ReplyInfo replyInfo)
{
Headers headers = replyInfo.getHeaders();
Headers.Header versionHeader = headers.get(HTTPSPDYHeader.STATUS.name(version));
if (versionHeader != null)
{
Matcher matcher = Pattern.compile("(\\d{3}).*").matcher(versionHeader.value());
if (matcher.matches() && Integer.parseInt(matcher.group(1)) < 400)
latch.countDown();
}
}
});
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
}
}

View File

@ -236,4 +236,9 @@ public class SPDYAsyncConnection extends AbstractConnection implements AsyncConn
{ {
this.session = session; this.session = session;
} }
public String toString()
{
return String.format("%s@%x{endp=%s@%x}",getClass().getSimpleName(),hashCode(),getEndPoint().getClass().getSimpleName(),getEndPoint().hashCode());
}
} }

View File

@ -16,6 +16,7 @@
package org.eclipse.jetty.spdy; package org.eclipse.jetty.spdy;
import java.io.IOException;
import java.nio.channels.SocketChannel; import java.nio.channels.SocketChannel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -41,6 +42,7 @@ import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.spdy.api.SPDY; import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener; import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener;
import org.eclipse.jetty.util.component.AggregateLifeCycle;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
@ -312,4 +314,14 @@ public class SPDYServerConnector extends SelectChannelConnector
threadPool.dispatch(command); threadPool.dispatch(command);
} }
} }
@Override
public void dump(Appendable out, String indent) throws IOException
{
super.dump(out,indent);
AggregateLifeCycle.dump(out, indent, new ArrayList<Session>(sessions));
}
} }

View File

@ -374,10 +374,10 @@ public class AggregateLifeCycle extends AbstractLifeCycle implements Destroyable
for (Bean b : _beans) for (Bean b : _beans)
{ {
i++; i++;
out.append(indent).append(" +- ");
if (b._managed) if (b._managed)
{ {
out.append(indent).append(" +- ");
if (b._bean instanceof Dumpable) if (b._bean instanceof Dumpable)
((Dumpable)b._bean).dump(out,indent+(i==size?" ":" | ")); ((Dumpable)b._bean).dump(out,indent+(i==size?" ":" | "));
else else

View File

@ -51,7 +51,10 @@ public abstract class WebSocketHandler extends HandlerWrapper implements WebSock
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{ {
if (_webSocketFactory.acceptWebSocket(request,response) || response.isCommitted()) if (_webSocketFactory.acceptWebSocket(request,response) || response.isCommitted())
{
baseRequest.setHandled(true);
return; return;
}
super.handle(target,baseRequest,request,response); super.handle(target,baseRequest,request,response);
} }