diff options
-rw-r--r-- | jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java | 137 | ||||
-rw-r--r-- | jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java | 175 |
2 files changed, 254 insertions, 58 deletions
diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java index cea7d0243c..5207f857b8 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java @@ -19,6 +19,8 @@ package org.eclipse.jetty.servlets; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; @@ -27,99 +29,118 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -/* ------------------------------------------------------------ */ -/** Concatenation Servlet - * This servlet may be used to concatenate multiple resources into - * a single response. It is intended to be used to load multiple +import org.eclipse.jetty.util.URIUtil; + +/** + * <p>This servlet may be used to concatenate multiple resources into + * a single response.</p> + * <p>It is intended to be used to load multiple * javascript or css files, but may be used for any content of the - * same mime type that can be meaningfully concatenated. - * <p> - * The servlet uses {@link RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} + * same mime type that can be meaningfully concatenated.</p> + * <p>The servlet uses {@link RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} * to combine the requested content, so dynamically generated content - * may be combined (Eg engine.js for DWR). - * <p> - * The servlet uses parameter names of the query string as resource names - * relative to the context root. So these script tags: + * may be combined (Eg engine.js for DWR).</p> + * <p>The servlet uses parameter names of the query string as resource names + * relative to the context root. So these script tags:</p> * <pre> - * <script type="text/javascript" src="../js/behaviour.js"></script> - * <script type="text/javascript" src="../js/ajax.js&/chat/chat.js"></script> - * <script type="text/javascript" src="../chat/chat.js"></script> - * </pre> can be replaced with the single tag (with the ConcatServlet mapped to /concat): + * <script type="text/javascript" src="../js/behaviour.js"></script> + * <script type="text/javascript" src="../js/ajax.js&/chat/chat.js"></script> + * <script type="text/javascript" src="../chat/chat.js"></script> + * </pre> + * <p>can be replaced with the single tag (with the {@code ConcatServlet} + * mapped to {@code /concat}):</p> * <pre> - * <script type="text/javascript" src="../concat?/js/behaviour.js&/js/ajax.js&/chat/chat.js"></script> + * <script type="text/javascript" src="../concat?/js/behaviour.js&/js/ajax.js&/chat/chat.js"></script> * </pre> - * The {@link ServletContext#getMimeType(String)} method is used to determine the - * mime type of each resource. If the types of all resources do not match, then a 415 - * UNSUPPORTED_MEDIA_TYPE error is returned. - * <p> - * If the init parameter "development" is set to "true" then the servlet will run in - * development mode and the content will be concatenated on every request. Otherwise - * the init time of the servlet is used as the lastModifiedTime of the combined content - * and If-Modified-Since requests are handled with 206 NOT Modified responses if + * <p>The {@link ServletContext#getMimeType(String)} method is used to determine the + * mime type of each resource. If the types of all resources do not match, then a 415 + * UNSUPPORTED_MEDIA_TYPE error is returned.</p> + * <p>If the init parameter {@code development} is set to {@code true} then the servlet + * will run in development mode and the content will be concatenated on every request.</p> + * <p>Otherwise the init time of the servlet is used as the lastModifiedTime of the combined content + * and If-Modified-Since requests are handled with 304 NOT Modified responses if * appropriate. This means that when not in development mode, the servlet must be - * restarted before changed content will be served. - * - * - * + * restarted before changed content will be served.</p> */ public class ConcatServlet extends HttpServlet { - boolean _development; - long _lastModified; - ServletContext _context; + private boolean _development; + private long _lastModified; - /* ------------------------------------------------------------ */ + @Override public void init() throws ServletException { - _lastModified=System.currentTimeMillis(); - _context=getServletContext(); - _development="true".equals(getInitParameter("development")); + _lastModified = System.currentTimeMillis(); + _development = Boolean.parseBoolean(getInitParameter("development")); } - /* ------------------------------------------------------------ */ /* * @return The start time of the servlet unless in development mode, in which case -1 is returned. */ + @Override protected long getLastModified(HttpServletRequest req) { - return _development?-1:_lastModified; + return _development ? -1 : _lastModified; } - /* ------------------------------------------------------------ */ - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String q=req.getQueryString(); - if (q==null) + String query = request.getQueryString(); + if (query == null) { - resp.sendError(HttpServletResponse.SC_NO_CONTENT); + response.sendError(HttpServletResponse.SC_NO_CONTENT); return; } - String[] parts = q.split("\\&"); - String type=null; - for (int i=0;i<parts.length;i++) + List<RequestDispatcher> dispatchers = new ArrayList<>(); + String[] parts = query.split("\\&"); + String type = null; + for (String part : parts) { - String t = _context.getMimeType(parts[i]); - if (t!=null) + String path = URIUtil.canonicalPath(URIUtil.decodePath(part)); + if (path == null) + { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + // Verify that the path is not protected. + if (startsWith(path, "/WEB-INF/") || startsWith(path, "/META-INF/")) + { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + String t = getServletContext().getMimeType(path); + if (t != null) { - if (type==null) - type=t; + if (type == null) + { + type = t; + } else if (!type.equals(t)) { - resp.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); return; } } + + RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(path); + if (dispatcher != null) + dispatchers.add(dispatcher); } - if (type!=null) - resp.setContentType(type); + if (type != null) + response.setContentType(type); - for (int i=0;i<parts.length;i++) - { - RequestDispatcher dispatcher=_context.getRequestDispatcher(parts[i]); - if (dispatcher!=null) - dispatcher.include(req,resp); - } + for (RequestDispatcher dispatcher : dispatchers) + dispatcher.include(request, response); + } + + private boolean startsWith(String path, String prefix) + { + // Case insensitive match. + return prefix.regionMatches(true, 0, path, 0, prefix.length()); } } diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java new file mode 100644 index 0000000000..ad58753db0 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java @@ -0,0 +1,175 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 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.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.webapp.WebAppContext; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class ConcatServletTest +{ + private Server server; + private LocalConnector connector; + + @Before + public void prepareServer() throws Exception + { + server = new Server(); + connector = new LocalConnector(server); + server.addConnector(connector); + } + + @After + public void destroy() throws Exception + { + if (server != null) + server.stop(); + } + + @Test + public void testConcatenation() throws Exception + { + String contextPath = ""; + ServletContextHandler context = new ServletContextHandler(server, contextPath); + server.setHandler(context); + String concatPath = "/concat"; + context.addServlet(ConcatServlet.class, concatPath); + ServletHolder resourceServletHolder = new ServletHolder(new HttpServlet() + { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + String includedURI = (String)request.getAttribute("javax.servlet.include.request_uri"); + response.getOutputStream().println(includedURI); + } + }); + context.addServlet(resourceServletHolder, "/resource/*"); + server.start(); + + String resource1 = "/resource/one.js"; + String resource2 = "/resource/two.js"; + String uri = contextPath + concatPath + "?" + resource1 + "&" + resource2; + String request = "" + + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + String response = connector.getResponses(request); + try (BufferedReader reader = new BufferedReader(new StringReader(response))) + { + while (true) + { + String line = reader.readLine(); + if (line == null) + Assert.fail(); + if (line.trim().isEmpty()) + break; + } + Assert.assertEquals(resource1, reader.readLine()); + Assert.assertEquals(resource2, reader.readLine()); + Assert.assertNull(reader.readLine()); + } + } + + @Test + public void testWEBINFResourceIsNotServed() throws Exception + { + File directoryFile = MavenTestingUtils.getTargetTestingDir(); + Path directoryPath = directoryFile.toPath(); + Path hiddenDirectory = directoryPath.resolve("WEB-INF"); + Files.createDirectories(hiddenDirectory); + Path hiddenResource = hiddenDirectory.resolve("one.js"); + try (OutputStream output = Files.newOutputStream(hiddenResource)) + { + output.write("function() {}".getBytes(StandardCharsets.UTF_8)); + } + + String contextPath = ""; + WebAppContext context = new WebAppContext(server, directoryPath.toString(), contextPath); + server.setHandler(context); + String concatPath = "/concat"; + context.addServlet(ConcatServlet.class, concatPath); + server.start(); + + // Verify that I can get the file programmatically, as required by the spec. + Assert.assertNotNull(context.getServletContext().getResource("/WEB-INF/one.js")); + + // Having a path segment and then ".." triggers a special case + // that the ConcatServlet must detect and avoid. + String uri = contextPath + concatPath + "?/trick/../WEB-INF/one.js"; + String request = "" + + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + String response = connector.getResponses(request); + Assert.assertTrue(response.startsWith("HTTP/1.1 404 ")); + + // Make sure ConcatServlet behaves well if it's case insensitive. + uri = contextPath + concatPath + "?/trick/../web-inf/one.js"; + request = "" + + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + response = connector.getResponses(request); + Assert.assertTrue(response.startsWith("HTTP/1.1 404 ")); + + // Make sure ConcatServlet behaves well if encoded. + uri = contextPath + concatPath + "?/trick/..%2FWEB-INF%2Fone.js"; + request = "" + + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + response = connector.getResponses(request); + Assert.assertTrue(response.startsWith("HTTP/1.1 404 ")); + + // Make sure ConcatServlet cannot see file system files. + uri = contextPath + concatPath + "?/trick/../../" + directoryFile.getName(); + request = "" + + "GET " + uri + " HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + response = connector.getResponses(request); + Assert.assertTrue(response.startsWith("HTTP/1.1 404 ")); + } +} |