diff --git a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/Http2Server.java b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/Http2Server.java index 3ebe2e2b5a4..51b5e1016c2 100644 --- a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/Http2Server.java +++ b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/Http2Server.java @@ -44,7 +44,7 @@ import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.servlets.PushCacheFilter; +import org.eclipse.jetty.servlets.PushSessionCacheFilter; import org.eclipse.jetty.util.ssl.SslContextFactory; @@ -59,7 +59,7 @@ public class Http2Server ServletContextHandler context = new ServletContextHandler(server, "/",ServletContextHandler.SESSIONS); context.setResourceBase("/tmp"); - context.addFilter(PushCacheFilter.class,"/*",EnumSet.of(DispatcherType.REQUEST)) + context.addFilter(PushSessionCacheFilter.class,"/*",EnumSet.of(DispatcherType.REQUEST)) .setInitParameter("ports","443,6443,8443"); context.addServlet(new ServletHolder(servlet), "/test/*"); context.addServlet(DefaultServlet.class, "/").setInitParameter("maxCacheSize","81920"); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java index 64d5bdb9155..e7f721faa70 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java @@ -217,6 +217,7 @@ public class Dispatcher implements RequestDispatcher } } + @Deprecated public void push(ServletRequest request) { Request baseRequest = Request.getBaseRequest(request); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilder.java b/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilder.java new file mode 100644 index 00000000000..3025e83c5ea --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilder.java @@ -0,0 +1,157 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.util.Collection; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.util.URIUtil; + + +/* ------------------------------------------------------------ */ +/** + * + */ +public class PushBuilder +{ + private final Request _request; + private final HttpFields _fields; + private String _method; + private String _queryString; + private String _sessionId; + private boolean _conditional; + + public PushBuilder(Request request, HttpFields fields, String method, String queryString, String sessionId, boolean conditional) + { + super(); + _request = request; + _fields = fields; + _method = method; + _queryString = queryString; + _sessionId = sessionId; + _conditional = conditional; + } + + public String getMethod() + { + return _method; + } + + public void setMethod(String method) + { + _method = method; + } + public String getQueryString() + { + return _queryString; + } + public void setQueryString(String queryString) + { + _queryString = queryString; + } + public String getSessionId() + { + return _sessionId; + } + public void setSessionId(String sessionId) + { + _sessionId = sessionId; + } + public boolean isConditional() + { + return _conditional; + } + public void setConditional(boolean conditional) + { + _conditional = conditional; + } + + public Collection getHeaderNames() + { + return _fields.getFieldNamesCollection(); + } + + public String getHeader(String name) + { + return _fields.get(name); + } + + public void setHeader(String name,String value) + { + _fields.put(name,value); + } + + public void addHeader(String name,String value) + { + _fields.add(name,value); + } + + /* ------------------------------------------------------------ */ + /** Push a resource. + * Push a resource based on the current state of the PushBuilder. If {@link #isConditional()} + * is true and an etag or lastModified value is provided, then an appropriate conditional header + * will be generated. If an etag and lastModified value are provided only an If-None-Match header + * will be generated. If the builder has a session ID, then the pushed request + * will include the session ID either as a Cookie or as a URI parameter as appropriate.The builders + * query string is merged with any passed query string. + * @param uriInContext The URI within the current context of the resource to push. + * @param etag The etag for the resource or null if not available + * @param lastModified The last modified date of the resource or null if not available + * @throws IllegalArgumentException if the method set expects a request + * body (eg POST) + */ + public void push(String uriInContext,String etag,String lastModified) + { + if (HttpMethod.POST.is(_method) || HttpMethod.PUT.is(_method)) + throw new IllegalStateException("Bad Method "+_method); + + String query=_queryString; + int q=uriInContext.indexOf('?'); + if (q>=0) + { + query=uriInContext.substring(q+1)+'&'+query; + uriInContext=uriInContext.substring(0,q); + } + + String path = URIUtil.addPaths(_request.getContextPath(),uriInContext); + + String param=null; + if (_sessionId!=null && _request.isRequestedSessionIdFromURL()) + param="jsessionid="+_sessionId; + + if (_conditional) + { + if (etag!=null) + _fields.add(HttpHeader.IF_NONE_MATCH,etag); + else if (lastModified!=null) + _fields.add(HttpHeader.IF_MODIFIED_SINCE,lastModified); + } + + HttpURI uri = HttpURI.createHttpURI(_request.getScheme(),_request.getServerName(),_request.getServerPort(),path,param,query,null); + MetaData.Request push = new MetaData.Request(_method,uri,_request.getHttpVersion(),_fields); + _request.getHttpChannel().getHttpTransport().push(push); + + } + + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 04852f824ad..a4f05a4c544 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -213,6 +213,110 @@ public class Request implements HttpServletRequest { return _input; } + + /* ------------------------------------------------------------ */ + /** Get a PushBuilder associated with this request initialized as follows: + * + *

Each call to getPushBuilder() will return a new instance + * of a PushBuilder based off this Request. Any mutations to the + * returned PushBuilder are not reflected on future returns. + * @return A new PushBuilder or null if push is not supported + */ + public PushBuilder getPushBuilder() + { + HttpFields fields = new HttpFields(getHttpFields().size()+5); + boolean conditional=false; + UserIdentity user_identity=null; + Authentication authentication=null; + + for (HttpField field : getHttpFields()) + { + HttpHeader header = field.getHeader(); + if (header==null) + fields.add(field); + else + { + switch(header) + { + case IF_MATCH: + case IF_RANGE: + case IF_UNMODIFIED_SINCE: + case RANGE: + case EXPECT: + case REFERER: + case COOKIE: + continue; + + case AUTHORIZATION: + user_identity=getUserIdentity(); + authentication=_authentication; + continue; + + case IF_NONE_MATCH: + case IF_MODIFIED_SINCE: + conditional=true; + continue; + + default: + fields.add(field); + } + } + } + + String id=null; + try + { + HttpSession session = getSession(); + if (session!=null) + { + session.getLastAccessedTime(); // checks if session is valid + id=session.getId(); + } + else + id=getRequestedSessionId(); + } + catch(IllegalStateException e) + { + id=getRequestedSessionId(); + } + + PushBuilder builder = new PushBuilder(this,fields,getMethod(),getQueryString(),id,conditional); + builder.addHeader("referer",getRequestURL().toString()); + + // TODO process any set cookies + // TODO process any user_identity + + return builder; + } /* ------------------------------------------------------------ */ public void addEventListener(final EventListener listener) diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PushSessionCacheFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PushSessionCacheFilter.java new file mode 100644 index 00000000000..1d2885cb265 --- /dev/null +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/PushSessionCacheFilter.java @@ -0,0 +1,193 @@ +// +// ======================================================================== +// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + + +package org.eclipse.jetty.servlets; + +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.server.PushBuilder; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + + +/* ------------------------------------------------------------ */ +/** + */ +public class PushSessionCacheFilter implements Filter +{ + private static final String TARGET_ATTR="PushCacheFilter.target"; + private static final Logger LOG = Log.getLogger(PushSessionCacheFilter.class); + private final ConcurrentMap _cache = new ConcurrentHashMap<>(); + + private long _associateDelay=5000L; + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) + */ + @Override + public void init(FilterConfig config) throws ServletException + { + if (config.getInitParameter("associateDelay")!=null) + _associateDelay=Long.valueOf(config.getInitParameter("associateDelay")); + + config.getServletContext().addListener(new ServletRequestListener() + { + @Override + public void requestDestroyed(ServletRequestEvent sre) + { + Request request = Request.getBaseRequest(sre.getServletRequest()); + Target target = (Target)request.getAttribute(TARGET_ATTR); + if (target==null) + return; + + // Update conditional data + Response response = request.getResponse(); + target._etag=response.getHttpFields().get(HttpHeader.ETAG); + target._lastModified=response.getHttpFields().get(HttpHeader.LAST_MODIFIED); + + // Does this request have a referer? + String referer = request.getHttpFields().get(HttpHeader.REFERER); + if (referer!=null) + { + // Is the referer from this contexts? + HttpURI uri = new HttpURI(referer); + String path = uri.getPath(); + if (request.getServerName().equals(uri.getHost()) && path.startsWith(request.getContextPath())) + { + String path_in_ctx = path.substring(request.getContextPath().length()); + Target referer_target = _cache.get(path_in_ctx); + if (referer_target!=null) + { + String sessionId = request.getSession(true).getId(); + Long last = referer_target._timestamp.get(sessionId); + if (last!=null && (System.currentTimeMillis()-last)<_associateDelay && !referer_target._associated.containsKey(path)) + { + if (referer_target._associated.putIfAbsent(path,target)==null) + LOG.info("ASSOCIATE {}->{}",path_in_ctx,target._path); + } + } + } + } + } + + @Override + public void requestInitialized(ServletRequestEvent sre) + { + } + + }); + + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException + { + Request baseRequest = Request.getBaseRequest(request); + + // Iterating over fields is more efficient than multiple gets + HttpFields fields = baseRequest.getHttpFields(); + String referer=fields.get(HttpHeader.REFERER); + + if (LOG.isDebugEnabled()) + LOG.debug("{} {} referer={}%n",baseRequest.getMethod(),baseRequest.getRequestURI(),referer); + + + HttpSession session = baseRequest.getSession(true); + String sessionId = session.getId(); + String path = URIUtil.addPaths(baseRequest.getServletPath(),baseRequest.getPathInfo()); + + // find the target for this resource + Target target = _cache.get(path); + if (target == null) + { + Target t=new Target(path); + target = _cache.putIfAbsent(path,t); + target = target==null?t:target; + } + target._timestamp.put(sessionId,System.currentTimeMillis()); + request.setAttribute(TARGET_ATTR,target); + + // push any associated resources + if (target._associated.size()>0) + { + PushBuilder builder = baseRequest.getPushBuilder(); + if (!session.isNew()) + builder.setConditional(true); + if (builder!=null) + { + for (Target associated : target._associated.values()) + { + LOG.info("PUSH {}->{}",path,associated._path); + builder.push(associated._path,associated._etag,associated._lastModified); + } + } + } + + chain.doFilter(request,response); + } + + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.Filter#destroy() + */ + @Override + public void destroy() + { + } + + + public static class Target + { + final String _path; + final ConcurrentMap _associated = new ConcurrentHashMap<>(); + final ConcurrentMap _timestamp = new ConcurrentHashMap<>(); + volatile String _etag; + volatile String _lastModified; + + public Target(String path) + { + _path=path; + } + + } +}