Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimone Bordet2015-07-16 13:10:12 -0400
committerSimone Bordet2015-07-16 13:10:12 -0400
commit5bb942b5c1c1ef22798b066fd97c4608f68c8b31 (patch)
tree4ca1c264ecf8cab68d02ca8534c5840c96745532 /jetty-servlets
parent01497cc530263bb71414e9b2b919c922b4ea6804 (diff)
parentdee941c365486b86ab742cb67c9b4bcf9c61ef2f (diff)
downloadorg.eclipse.jetty.project-5bb942b5c1c1ef22798b066fd97c4608f68c8b31.tar.gz
org.eclipse.jetty.project-5bb942b5c1c1ef22798b066fd97c4608f68c8b31.tar.xz
org.eclipse.jetty.project-5bb942b5c1c1ef22798b066fd97c4608f68c8b31.zip
Merged branch 'jetty-9.2.x' into 'master'.
Diffstat (limited to 'jetty-servlets')
-rw-r--r--jetty-servlets/src/main/java/org/eclipse/jetty/servlets/ConcatServlet.java138
-rw-r--r--jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ConcatServletTest.java175
2 files changed, 254 insertions, 59 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 76ac313b2a..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,100 +29,118 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-/**
- * Concatenation Servlet
- * <p>
- * 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>
- * &lt;script type="text/javascript" src="../js/behaviour.js"&gt;&lt;/script&gt;
- * &lt;script type="text/javascript" src="../js/ajax.js&amp;/chat/chat.js"&gt;&lt;/script&gt;
- * &lt;script type="text/javascript" src="../chat/chat.js"&gt;&lt;/script&gt;
- * </pre> can be replaced with the single tag (with the ConcatServlet mapped to /concat):
+ * &lt;script type="text/javascript" src="../js/behaviour.js"&gt;&lt;/script&gt;
+ * &lt;script type="text/javascript" src="../js/ajax.js&/chat/chat.js"&gt;&lt;/script&gt;
+ * &lt;script type="text/javascript" src="../chat/chat.js"&gt;&lt;/script&gt;
+ * </pre>
+ * <p>can be replaced with the single tag (with the {@code ConcatServlet}
+ * mapped to {@code /concat}):</p>
* <pre>
- * &lt;script type="text/javascript" src="../concat?/js/behaviour.js&amp;/js/ajax.js&amp;/chat/chat.js"&gt;&lt;/script&gt;
+ * &lt;script type="text/javascript" src="../concat?/js/behaviour.js&/js/ajax.js&/chat/chat.js"&gt;&lt;/script&gt;
* </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 "));
+ }
+}

Back to the top