diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractBuffers.java b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractBuffers.java index b94710af5ca..9bc63cd1deb 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractBuffers.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractBuffers.java @@ -1,4 +1,16 @@ 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.IndirectNIOBuffer; diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java index ef80cccb2ff..e3da77460e4 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractConnection.java @@ -1,4 +1,16 @@ 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; diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java index a8db4ec100b..3ba70b035b2 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java @@ -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())) { synchronized (this) - { - if (_dispatched) - _writable=false; + { + _writable=false; + if (!_dispatched) + updateKey(); } } else if (l>0) @@ -358,9 +359,10 @@ public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPo if (l==0 && buffer!=null && buffer.hasContent()) { synchronized (this) - { - if (_dispatched) - _writable=false; + { + _writable=false; + if (!_dispatched) + updateKey(); } } else if (l>0) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java index a6537730dfd..f477da9c174 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java @@ -354,7 +354,7 @@ public class Server extends HandlerWrapper implements Attributes { LOG.debug("REQUEST "+target+" on "+connection); handle(target, request, request, response); - LOG.debug("RESPONSE "+target+" "+connection.getResponse().getStatus()); + LOG.debug("RESPONSE "+target+" "+connection.getResponse().getStatus()+" handled="+request.isHandled()); } else handle(target, request, request, response); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ConnectHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ConnectHandler.java index dba192ee8f0..9feb6f6a258 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ConnectHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ConnectHandler.java @@ -233,21 +233,21 @@ public class ConnectHandler extends HandlerWrapper } catch (SocketException se) { - LOG.info("ConnectHandler: " + se.getMessage()); - response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT); + LOG.info("ConnectHandler: SocketException " + se.getMessage()); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); baseRequest.setHandled(true); return; } catch (SocketTimeoutException ste) { - LOG.info("ConnectHandler: " + ste.getMessage()); + LOG.info("ConnectHandler: SocketTimeoutException" + ste.getMessage()); response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT); baseRequest.setHandled(true); return; } catch (IOException ioe) { - LOG.info("ConnectHandler: " + ioe.getMessage()); + LOG.info("ConnectHandler: IOException" + ioe.getMessage()); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); baseRequest.setHandled(true); return; diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/BalancerServlet.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/BalancerServlet.java new file mode 100644 index 00000000000..f7ef7db0097 --- /dev/null +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/BalancerServlet.java @@ -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 + { + + private BalancerMember[] _balancerMembers; + + private AtomicInteger _index; + + public RoundRobinIterator(Collection 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 FORBIDDEN_CONFIG_PARAMETERS; + static + { + List params = new LinkedList(); + params.add("HostHeader"); + params.add("whiteList"); + params.add("blackList"); + FORBIDDEN_CONFIG_PARAMETERS = Collections.unmodifiableList(params); + } + + private static final List REVERSE_PROXY_HEADERS; + static + { + List params = new LinkedList(); + 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 _balancerMembers = new HashSet(); + + 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 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 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 getBalancerNames(ServletConfig config) throws ServletException + { + Set names = new HashSet(); + @SuppressWarnings("unchecked") + List 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; + } + +} \ No newline at end of file diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java index 7fa971838a5..6687903a6a5 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ProxyServlet.java @@ -483,13 +483,20 @@ public class ProxyServlet implements Servlet @Override 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 (debug != 0) _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) _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. - * + * * This convenience extension to ProxyServlet configures the servlet as a transparent proxy. The servlet is configured with init parameters: *
    *
  • ProxyTo - a URI like http://host:80/context to which the request is proxied. @@ -795,7 +816,7 @@ public class ProxyServlet implements Servlet *
* 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 - * + * */ public static class Transparent extends ProxyServlet { diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractBalancerServletTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractBalancerServletTest.java new file mode 100644 index 00000000000..e798396adc8 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractBalancerServletTest.java @@ -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 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(); + } + +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BalancerServletTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BalancerServletTest.java new file mode 100644 index 00000000000..9513895fc61 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BalancerServletTest.java @@ -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"); + } + } + } + +} diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java index 446a9103e98..0ed914d709b 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardSession.java @@ -16,9 +16,11 @@ package org.eclipse.jetty.spdy; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.InterruptedByTimeoutException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; 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.parser.Parser; 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.Logger; -public class StandardSession implements ISession, Parser.Listener, Handler +public class StandardSession implements ISession, Parser.Listener, Handler, Dumpable { private static final Logger logger = Log.getLogger(Session.class); private static final ThreadLocal handlerInvocations = new ThreadLocal() @@ -1092,6 +1096,27 @@ public class StandardSession implements ISession, Parser.Listener, Handler { public IStream getStream(); diff --git a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java index d1731409681..a99536fa1fd 100644 --- a/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java +++ b/jetty-spdy/spdy-core/src/main/java/org/eclipse/jetty/spdy/StandardStream.java @@ -440,7 +440,7 @@ public class StandardStream implements IStream @Override 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() diff --git a/jetty-spdy/spdy-jetty-http-webapp/src/main/config/etc/jetty-spdy.xml b/jetty-spdy/spdy-jetty-http-webapp/src/main/config/etc/jetty-spdy.xml index 4218d4630e4..0d847bcbd48 100644 --- a/jetty-spdy/spdy-jetty-http-webapp/src/main/config/etc/jetty-spdy.xml +++ b/jetty-spdy/spdy-jetty-http-webapp/src/main/config/etc/jetty-spdy.xml @@ -11,11 +11,45 @@ TLSv1 + + + + + + 8080 @@ -26,6 +60,24 @@ + + 8443 diff --git a/jetty-spdy/spdy-jetty-http/pom.xml b/jetty-spdy/spdy-jetty-http/pom.xml index f091eb6b32d..dfaf1b1f937 100644 --- a/jetty-spdy/spdy-jetty-http/pom.xml +++ b/jetty-spdy/spdy-jetty-http/pom.xml @@ -72,6 +72,11 @@ ${slf4j-version} test + + org.mockito + mockito-core + test + diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/HTTPSPDYServerConnector.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/HTTPSPDYServerConnector.java index 2cf6e68fd48..389fdb90e28 100644 --- a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/HTTPSPDYServerConnector.java +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/HTTPSPDYServerConnector.java @@ -16,6 +16,9 @@ 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.util.ssl.SslContextFactory; @@ -23,32 +26,41 @@ public class HTTPSPDYServerConnector extends AbstractHTTPSPDYServerConnector { public HTTPSPDYServerConnector() { - this(null, new PushStrategy.None()); + this(null, Collections.emptyMap()); } - public HTTPSPDYServerConnector(PushStrategy pushStrategy) + public HTTPSPDYServerConnector(Map pushStrategies) { - this(null, pushStrategy); + this(null, pushStrategies); } public HTTPSPDYServerConnector(SslContextFactory sslContextFactory) { - this(sslContextFactory, new PushStrategy.None()); + this(sslContextFactory, Collections.emptyMap()); } - public HTTPSPDYServerConnector(SslContextFactory sslContextFactory, PushStrategy pushStrategy) + public HTTPSPDYServerConnector(SslContextFactory sslContextFactory, Map pushStrategies) { // We pass a null ServerSessionFrameListener because for // HTTP over SPDY we need one that references the endPoint super(null, sslContextFactory); clearAsyncConnectionFactories(); // 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 - 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 putAsyncConnectionFactory("http/1.1", new ServerHTTPAsyncConnectionFactory(this)); // The default connection factory handles plain HTTP on non-SSL or non-NPN connections setDefaultAsyncConnectionFactory(getAsyncConnectionFactory("http/1.1")); } + + private PushStrategy getPushStrategy(short version, Map pushStrategies) + { + PushStrategy pushStrategy = pushStrategies.get(version); + if(pushStrategy == null) + pushStrategy = new PushStrategy.None(); + return pushStrategy; + } + } diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java index 52f1243d734..0d7857f931a 100644 --- a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategy.java @@ -23,6 +23,8 @@ import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Pattern; import org.eclipse.jetty.spdy.api.Headers; @@ -37,12 +39,13 @@ import org.eclipse.jetty.util.log.Logger; * will have a Referer HTTP header that points to index.html, which we * use to link the associated resource to the main resource.

*

However, also following a hyperlink generates a HTTP request with a Referer - * HTTP header that points to index.html; therefore main resources and associated - * resources must be distinguishable.

- *

This class distinguishes associated resources by their URL path suffix and content + * HTTP header that points to index.html; therefore a proper value for {@link #getReferrerPushPeriod()} + * has to be set. If the referrerPushPeriod for a main resource has been passed, no more + * associated resources will be added for that main resource.

+ *

This class distinguishes associated main resources by their URL path suffix and content * type. * CSS stylesheets, images and JavaScript files have recognizable URL path suffixes that - * are classified as associated resources.

+ * are classified as associated resources. The suffix regexs can be configured by constructor argument

*

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.

*

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 { private static final Logger logger = Log.getLogger(ReferrerPushStrategy.class); - private final ConcurrentMap> resources = new ConcurrentHashMap<>(); + private final ConcurrentMap mainResources = new ConcurrentHashMap<>(); private final Set pushRegexps = new HashSet<>(); private final Set pushContentTypes = new HashSet<>(); private final Set allowedPushOrigins = new HashSet<>(); private volatile int maxAssociatedResources = 32; + private volatile int referrerPushPeriod = 5000; public ReferrerPushStrategy() { @@ -101,22 +105,33 @@ public class ReferrerPushStrategy implements PushStrategy this.maxAssociatedResources = maxAssociatedResources; } + public int getReferrerPushPeriod() + { + return referrerPushPeriod; + } + + public void setReferrerPushPeriod(int referrerPushPeriod) + { + this.referrerPushPeriod = referrerPushPeriod; + } + @Override public Set apply(Stream stream, Headers requestHeaders, Headers responseHeaders) { - Set result = Collections.emptySet(); + Set result = Collections.emptySet(); short version = stream.getSession().getVersion(); - String scheme = requestHeaders.get(HTTPSPDYHeader.SCHEME.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())) + if (!isIfModifiedSinceHeaderPresent(requestHeaders) && 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)) { - result = pushResources(absoluteURL); + MainResource mainResource = getOrCreateMainResource(absoluteURL); + result = mainResource.getResources(); } else if (isPushResource(url, responseHeaders)) { @@ -124,18 +139,49 @@ public class ReferrerPushStrategy implements PushStrategy if (referrerHeader != null) { String referrer = referrerHeader.value(); - Set pushResources = resources.get(referrer); - if (pushResources == null || !pushResources.contains(url)) - buildMetadata(origin, url, referrer); + MainResource mainResource = mainResources.get(referrer); + if (mainResource == null) + mainResource = getOrCreateMainResource(referrer); + + Set pushResources = mainResource.getResources(); + if (!pushResources.contains(url)) + mainResource.addResource(url, origin, referrer); 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; } + private Set getPushResources(String absoluteURL) + { + Set 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) { return "GET".equalsIgnoreCase(method); @@ -165,49 +211,71 @@ public class ReferrerPushStrategy implements PushStrategy return false; } - private Set pushResources(String absoluteURL) + private class MainResource { - Set pushResources = resources.get(absoluteURL); - if (pushResources == null) - return Collections.emptySet(); - return Collections.unmodifiableSet(pushResources); - } + private final String name; + private final Set resources = Collections.newSetFromMap(new ConcurrentHashMap()); + private final AtomicLong firstResourceAdded = new AtomicLong(-1); - private void buildMetadata(String origin, String url, String referrer) - { - if (referrer.startsWith(origin) || isPushOriginAllowed(origin)) + private MainResource(String name) { - Set pushResources = resources.get(referrer); - if (pushResources == null) + this.name = name; + } + + 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()); - Set existing = resources.putIfAbsent(referrer, pushResources); - if (existing != null) - pushResources = existing; + logger.debug("Skipped store of push metadata {} for {}: Origin: {} doesn't match or origin not allowed", + url, name, origin); + return false; } + // This check is not strictly concurrent-safe, but limiting // the number of associated resources is achieved anyway // although in rare cases few more resources will be stored - if (pushResources.size() < getMaxAssociatedResources()) - { - pushResources.add(url); - logger.debug("Stored push metadata for {}: {}", referrer, pushResources); - } - else + if (resources.size() >= maxAssociatedResources) { 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) - { - for (Pattern allowedPushOrigin : allowedPushOrigins) - { - if (allowedPushOrigin.matcher(origin).matches()) - return true; + logger.debug("Adding resource: {} for: {} with delay: {}ms.", url, name, delay); + resources.add(url); + return true; + } + + public Set 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; } } diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYAsyncConnection.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYAsyncConnection.java index 5fb09f555c9..0c3af1bb083 100644 --- a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYAsyncConnection.java +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/http/ServerHTTPSPDYAsyncConnection.java @@ -55,6 +55,7 @@ import org.eclipse.jetty.spdy.api.Handler; import org.eclipse.jetty.spdy.api.Headers; import org.eclipse.jetty.spdy.api.ReplyInfo; 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.StreamStatus; import org.eclipse.jetty.spdy.api.SynInfo; @@ -177,6 +178,10 @@ public class ServerHTTPSPDYAsyncConnection extends AbstractHttpConnection implem logger.debug("HTTP > {} {} {}", m, u, 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); handle(); break; @@ -403,7 +408,7 @@ public class ServerHTTPSPDYAsyncConnection extends AbstractHttpConnection implem if (!stream.isUnidirectional()) stream.reply(replyInfo); 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 @@ -411,19 +416,12 @@ public class ServerHTTPSPDYAsyncConnection extends AbstractHttpConnection implem Headers.Header host = headers.get(HTTPSPDYHeader.HOST.name(version)); Headers.Header uri = headers.get(HTTPSPDYHeader.URI.name(version)); Set 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(); - pushHeaders.put(HTTPSPDYHeader.METHOD.name(version), "GET"); - 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")); + final Headers requestHeaders = createRequestHeaders(scheme, host, uri, pushResourcePath); + final Headers pushHeaders = createPushHeaders(scheme, host, pushResourcePath); + stream.syn(new SynInfo(pushHeaders, false), getMaxIdleTime(), TimeUnit.MILLISECONDS, new Handler.Adapter() { @Override @@ -431,16 +429,43 @@ public class ServerHTTPSPDYAsyncConnection extends AbstractHttpConnection implem { ServerHTTPSPDYAsyncConnection pushConnection = 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 diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyEngine.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyEngine.java index 1013430f17f..14d053a394d 100644 --- a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyEngine.java +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/ProxyEngine.java @@ -25,6 +25,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; 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.server.ServerSessionFrameListener; import org.eclipse.jetty.util.log.Log; @@ -71,19 +72,27 @@ public abstract class ProxyEngine extends ServerSessionFrameListener.Adapter imp return name; } - protected void addRequestProxyHeaders(Headers headers) + protected void addRequestProxyHeaders(Stream stream, Headers headers) { - String newValue = ""; - Headers.Header header = headers.get("via"); - if (header != null) - newValue = header.valuesAsString() + ", "; - newValue += "http/1.1 " + getName(); - headers.put("via", newValue); + addViaHeader(headers); } - 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 getProxyInfos() diff --git a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/SPDYProxyEngine.java b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/SPDYProxyEngine.java index 23b38b0cdbb..55cce5d4d3b 100644 --- a/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/SPDYProxyEngine.java +++ b/jetty-spdy/spdy-jetty-http/src/main/java/org/eclipse/jetty/spdy/proxy/SPDYProxyEngine.java @@ -130,8 +130,6 @@ public class SPDYProxyEngine extends ProxyEngine return null; } - // TODO: give a chance to modify headers and rewrite URI - short serverVersion = proxyInfo.getVersion(); InetSocketAddress address = proxyInfo.getAddress(); Session serverSession = produceSession(host, serverVersion, address); @@ -145,15 +143,13 @@ public class SPDYProxyEngine extends ProxyEngine Set sessions = (Set)serverSession.getAttribute(CLIENT_SESSIONS_ATTRIBUTE); sessions.add(clientSession); + addRequestProxyHeaders(clientStream, headers); + customizeRequestHeaders(clientStream, headers); convert(clientVersion, serverVersion, headers); - addRequestProxyHeaders(headers); - SynInfo serverSynInfo = new SynInfo(headers, clientSynInfo.isClose()); - logger.debug("P -> S {}", serverSynInfo); - StreamFrameListener listener = new ProxyStreamFrameListener(clientStream); - StreamHandler handler = new StreamHandler(clientStream); + StreamHandler handler = new StreamHandler(clientStream, serverSynInfo); clientStream.setAttribute(STREAM_HANDLER_ATTRIBUTE, handler); serverSession.syn(serverSynInfo, listener, timeout, TimeUnit.MILLISECONDS, handler); return this; @@ -254,16 +250,19 @@ public class SPDYProxyEngine extends ProxyEngine @Override public void onReply(final Stream stream, ReplyInfo replyInfo) { + logger.debug("S -> P {} on {}", replyInfo, stream); + short serverVersion = stream.getSession().getVersion(); Headers headers = new Headers(replyInfo.getHeaders(), false); + + addResponseProxyHeaders(stream, headers); + customizeResponseHeaders(stream, headers); short clientVersion = this.clientStream.getSession().getVersion(); convert(serverVersion, clientVersion, headers); - addResponseProxyHeaders(headers); - this.replyInfo = new ReplyInfo(headers, replyInfo.isClose()); if (replyInfo.isClose()) - reply(); + reply(stream); } @Override @@ -276,19 +275,29 @@ public class SPDYProxyEngine extends ProxyEngine @Override public void onData(final Stream stream, final DataInfo dataInfo) { + logger.debug("S -> P {} on {}", dataInfo, stream); + if (replyInfo != null) { if (dataInfo.isClose()) 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() + final ReplyInfo replyInfo = this.replyInfo; + this.replyInfo = null; + clientStream.reply(replyInfo, getTimeout(), TimeUnit.MILLISECONDS, new Handler() { + @Override + public void completed(Void context) + { + logger.debug("P -> C {} from {} to {}", replyInfo, stream, clientStream); + } + @Override public void failed(Void context, Throwable x) { @@ -296,10 +305,9 @@ public class SPDYProxyEngine extends ProxyEngine 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() { @@ -307,6 +315,7 @@ public class SPDYProxyEngine extends ProxyEngine public void completed(Void context) { dataInfo.consume(dataInfo.length()); + logger.debug("P -> C {} from {} to {}", dataInfo, stream, clientStream); } @Override @@ -331,16 +340,20 @@ public class SPDYProxyEngine extends ProxyEngine { private final Queue queue = new LinkedList<>(); private final Stream clientStream; + private final SynInfo serverSynInfo; private Stream serverStream; - private StreamHandler(Stream clientStream) + private StreamHandler(Stream clientStream, SynInfo serverSynInfo) { this.clientStream = clientStream; + this.serverSynInfo = serverSynInfo; } @Override public void completed(Stream serverStream) { + logger.debug("P -> S {} from {} to {}", serverSynInfo, clientStream, serverStream); + serverStream.setAttribute(CLIENT_STREAM_ATTRIBUTE, clientStream); DataInfoHandler dataInfoHandler; @@ -470,14 +483,15 @@ public class SPDYProxyEngine extends ProxyEngine Headers headers = new Headers(serverSynInfo.getHeaders(), false); + addResponseProxyHeaders(serverStream, headers); + customizeResponseHeaders(serverStream, headers); Stream clientStream = (Stream)serverStream.getAssociatedStream().getAttribute(CLIENT_STREAM_ATTRIBUTE); convert(serverStream.getSession().getVersion(), clientStream.getSession().getVersion(), headers); - addResponseProxyHeaders(headers); - - StreamHandler handler = new StreamHandler(clientStream); + StreamHandler handler = new StreamHandler(clientStream, serverSynInfo); serverStream.setAttribute(STREAM_HANDLER_ATTRIBUTE, handler); clientStream.syn(new SynInfo(headers, serverSynInfo.isClose()), getTimeout(), TimeUnit.MILLISECONDS, handler); + return this; } diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/PushStrategyBenchmarkTest.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/PushStrategyBenchmarkTest.java index 29b5952d4e5..4ca4a65e6b3 100644 --- a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/PushStrategyBenchmarkTest.java +++ b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/PushStrategyBenchmarkTest.java @@ -137,39 +137,21 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest for (int i = 0; i < cssResources.length; ++i) { String path = "/" + i + ".css"; - 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); + exchange = createExchangeWithReferrer(referrer, path); ++result; httpClient.send(exchange); } for (int i = 0; i < jsResources.length; ++i) { String path = "/" + i + ".js"; - 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); + exchange = createExchangeWithReferrer(referrer, path); ++result; httpClient.send(exchange); } for (int i = 0; i < pngResources.length; ++i) { String path = "/" + i + ".png"; - 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); + exchange = createExchangeWithReferrer(referrer, path); ++result; httpClient.send(exchange); } @@ -180,6 +162,19 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest 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 { @@ -238,13 +233,7 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest String path = "/" + i + ".css"; if (pushedResources.contains(path)) continue; - 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); + headers = createRequestHeaders(referrer, path); ++result; session.syn(new SynInfo(headers, true), new DataListener()); } @@ -253,13 +242,7 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest String path = "/" + i + ".js"; if (pushedResources.contains(path)) continue; - 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); + headers = createRequestHeaders(referrer, path); ++result; session.syn(new SynInfo(headers, true), new DataListener()); } @@ -268,13 +251,7 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest String path = "/" + i + ".png"; if (pushedResources.contains(path)) continue; - 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); + headers = createRequestHeaders(referrer, path); ++result; session.syn(new SynInfo(headers, true), new DataListener()); } @@ -285,6 +262,19 @@ public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest 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 { try diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyUnitTest.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyUnitTest.java new file mode 100644 index 00000000000..a1df6dcced1 --- /dev/null +++ b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyUnitTest.java @@ -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 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 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; + } +} diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyV2Test.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyV2Test.java index ce88e712c74..ab24521bea6 100644 --- a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyV2Test.java +++ b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/ReferrerPushStrategyV2Test.java @@ -7,7 +7,7 @@ * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software + * Unless required by ap‰plicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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.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.SessionFrameListener; import org.eclipse.jetty.spdy.api.Stream; @@ -42,6 +43,10 @@ import org.junit.Test; public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest { + + private final String mainResource = "/index.html"; + private final String cssResource = "/style.css"; + @Override protected SPDYServerConnector newHTTPSPDYServerConnector(short version) { @@ -51,10 +56,71 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest 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 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 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); } }); - 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); 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() { @Override @@ -98,13 +156,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); final CountDownLatch associatedResourceLatch1 = new CountDownLatch(1); - Headers associatedRequestHeaders1 = new Headers(); - 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); + Headers associatedRequestHeaders1 = createHeaders(cssResource); session1.syn(new SynInfo(associatedRequestHeaders1, true), new StreamFrameListener.Adapter() { @Override @@ -116,15 +168,15 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest } }); Assert.assertTrue(associatedResourceLatch1.await(5, TimeUnit.SECONDS)); + return session1; + } + + private void sendJSRequest(Session session1) throws InterruptedException + { final CountDownLatch associatedResourceLatch2 = new CountDownLatch(1); - Headers associatedRequestHeaders2 = new Headers(); - associatedRequestHeaders2.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); - 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); + String jsResource = "/application.js"; + Headers associatedRequestHeaders2 = createHeaders(jsResource); session1.syn(new SynInfo(associatedRequestHeaders2, true), new StreamFrameListener.Adapter() { @Override @@ -136,17 +188,24 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest } }); 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, // we expect the css being pushed, but not the js final CountDownLatch mainStreamLatch = new CountDownLatch(2); final CountDownLatch pushDataLatch = new CountDownLatch(1); + final CountDownLatch pushSynHeadersValid = new CountDownLatch(1); Session session2 = startClient(version(), address, new SessionFrameListener.Adapter() { @Override public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) { + if(validateHeaders) + validateHeaders(synInfo.getHeaders(), pushSynHeadersValid); + Assert.assertTrue(stream.isUnidirectional()); Assert.assertTrue(synInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version())).value().endsWith(".css")); return new StreamFrameListener.Adapter() @@ -180,8 +239,10 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest } }); - Assert.assertTrue(mainStreamLatch.await(5, TimeUnit.SECONDS)); - Assert.assertTrue(pushDataLatch.await(5, TimeUnit.SECONDS)); + Assert.assertTrue("Main request reply and/or data not received", mainStreamLatch.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 @@ -204,13 +265,8 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Session session1 = startClient(version(), address, null); 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()); + Headers mainRequestHeaders = createHeadersWithoutReferrer(mainResource); + session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() { @Override @@ -224,13 +280,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); final CountDownLatch associatedResourceLatch = new CountDownLatch(1); - Headers associatedRequestHeaders = new Headers(); - 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); + Headers associatedRequestHeaders = createHeaders(cssResource); session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() { @Override @@ -290,6 +340,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest @Test public void testAssociatedResourceWithWrongContentTypeIsNotPushed() throws Exception { + final String fakeResource = "/fake.png"; InetSocketAddress address = startHTTPServer(version(), new AbstractHandler() { @Override @@ -302,7 +353,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest response.setContentType("text/html"); output.print("HELLO"); } - else if (url.equals("/fake.png")) + else if (url.equals(fakeResource)) { response.setContentType("text/html"); output.print("IMAGE"); @@ -318,13 +369,8 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Session session1 = startClient(version(), address, null); 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()); + Headers mainRequestHeaders = createHeadersWithoutReferrer(mainResource); + session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() { @Override @@ -338,13 +384,8 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); final CountDownLatch associatedResourceLatch = new CountDownLatch(1); - Headers associatedRequestHeaders = new Headers(); - associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); - 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); + String cssResource = "/stylesheet.css"; + Headers associatedRequestHeaders = createHeaders(cssResource); session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() { @Override @@ -358,13 +399,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); final CountDownLatch fakeAssociatedResourceLatch = new CountDownLatch(1); - Headers fakeAssociatedRequestHeaders = new Headers(); - 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); + Headers fakeAssociatedRequestHeaders = createHeaders(fakeResource); session1.syn(new SynInfo(fakeAssociatedRequestHeaders, true), new StreamFrameListener.Adapter() { @Override @@ -445,13 +480,8 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Session session1 = startClient(version(), address, null); 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()); + Headers mainRequestHeaders = createHeadersWithoutReferrer(mainResource); + session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() { @Override @@ -465,14 +495,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); final CountDownLatch associatedResourceLatch = new CountDownLatch(1); - Headers associatedRequestHeaders = new Headers(); - 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); + Headers associatedRequestHeaders = createHeaders(cssResource); session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() { @Override @@ -486,13 +509,9 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Assert.assertTrue(associatedResourceLatch.await(5, TimeUnit.SECONDS)); final CountDownLatch nestedResourceLatch = new CountDownLatch(1); - Headers nestedRequestHeaders = new Headers(); - nestedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); - 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); + String imageUrl = "/image.gif"; + Headers nestedRequestHeaders = createHeaders(imageUrl, cssResource); + session1.syn(new SynInfo(nestedRequestHeaders, true), new StreamFrameListener.Adapter() { @Override @@ -567,13 +586,8 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Session session1 = startClient(version(), address, null); 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()); + Headers mainRequestHeaders = createHeadersWithoutReferrer(mainResource); + session1.syn(new SynInfo(mainRequestHeaders, true), new StreamFrameListener.Adapter() { @Override @@ -587,13 +601,9 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Assert.assertTrue(mainResourceLatch.await(5, TimeUnit.SECONDS)); final CountDownLatch associatedResourceLatch = new CountDownLatch(1); - Headers associatedRequestHeaders = new Headers(); - associatedRequestHeaders.put(HTTPSPDYHeader.METHOD.name(version()), "GET"); - 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); + String associatedResource = "/home.html"; + Headers associatedRequestHeaders = createHeaders(associatedResource); + session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() { @Override @@ -661,13 +671,7 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest Session session1 = startClient(version(), address, null); 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()); + Headers mainRequestHeaders = createHeaders(mainResource); mainRequestHeaders.put("If-Modified-Since", "Tue, 27 Mar 2012 16:36:52 GMT"); 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)); final CountDownLatch associatedResourceLatch = new CountDownLatch(1); - Headers associatedRequestHeaders = new Headers(); - 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); + Headers associatedRequestHeaders = createHeaders(cssResource); session1.syn(new SynInfo(associatedRequestHeaders, true), new StreamFrameListener.Adapter() { @Override @@ -745,4 +743,57 @@ public class ReferrerPushStrategyV2Test extends AbstractHTTPSPDYTest 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)); } + + 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; + } } diff --git a/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/SSLExternalServerTest.java b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/SSLExternalServerTest.java new file mode 100644 index 00000000000..d27bf4845eb --- /dev/null +++ b/jetty-spdy/spdy-jetty-http/src/test/java/org/eclipse/jetty/spdy/http/SSLExternalServerTest.java @@ -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)); + } +} diff --git a/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYAsyncConnection.java b/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYAsyncConnection.java index e6df8fd3fde..3712138a062 100644 --- a/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYAsyncConnection.java +++ b/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYAsyncConnection.java @@ -236,4 +236,9 @@ public class SPDYAsyncConnection extends AbstractConnection implements AsyncConn { this.session = session; } + + public String toString() + { + return String.format("%s@%x{endp=%s@%x}",getClass().getSimpleName(),hashCode(),getEndPoint().getClass().getSimpleName(),getEndPoint().hashCode()); + } } diff --git a/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYServerConnector.java b/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYServerConnector.java index 3226ccadeaa..31a29ca0d0c 100644 --- a/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYServerConnector.java +++ b/jetty-spdy/spdy-jetty/src/main/java/org/eclipse/jetty/spdy/SPDYServerConnector.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.spdy; +import java.io.IOException; import java.nio.channels.SocketChannel; import java.util.ArrayList; 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.Session; 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.Logger; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -312,4 +314,14 @@ public class SPDYServerConnector extends SelectChannelConnector threadPool.dispatch(command); } } + + + @Override + public void dump(Appendable out, String indent) throws IOException + { + super.dump(out,indent); + AggregateLifeCycle.dump(out, indent, new ArrayList(sessions)); + } + + } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/component/AggregateLifeCycle.java b/jetty-util/src/main/java/org/eclipse/jetty/util/component/AggregateLifeCycle.java index 917d9ddfc71..4a4f2f6b583 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/component/AggregateLifeCycle.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/component/AggregateLifeCycle.java @@ -374,10 +374,10 @@ public class AggregateLifeCycle extends AbstractLifeCycle implements Destroyable for (Bean b : _beans) { i++; - + + out.append(indent).append(" +- "); if (b._managed) { - out.append(indent).append(" +- "); if (b._bean instanceof Dumpable) ((Dumpable)b._bean).dump(out,indent+(i==size?" ":" | ")); else diff --git a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketHandler.java b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketHandler.java index d90780f5c3d..5e4bc38582f 100644 --- a/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketHandler.java +++ b/jetty-websocket/src/main/java/org/eclipse/jetty/websocket/WebSocketHandler.java @@ -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 { if (_webSocketFactory.acceptWebSocket(request,response) || response.isCommitted()) + { + baseRequest.setHandled(true); return; + } super.handle(target,baseRequest,request,response); }