diff --git a/VERSION.txt b/VERSION.txt index d53dba269d7..8a973b743fa 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -108,7 +108,6 @@ jetty-9.0.4.v20130625 - 25 June 2013 + 410405 Avoid NPE for requestDispatcher(../) + 410469 UpgradeRequest is sent twice when using SSL, one fails warning about WritePendingException - + 410498 ignore type of exception in GoAwayTest.testDataNotProcessedAfterGoAway + 410522 jetty start broken for command line options + 410537 Exceptions during @OnWebSocketConnect not reported to @OnWebSocketError @@ -301,6 +300,75 @@ jetty-9.0.0.v20130308 - 08 March 2013 + 402757 WebSocket client module can't be used with WebSocket server module in the same WAR +jetty-8.1.12.v20130726 - 26 July 2013 + + 396706 CGI support parameters + + 397193 MongoSessionManager refresh updates last access time + + 407342 ReloadedSessionMissingClassTest uses class compiled with jdk7 + + 408529 Etags set in 304 response + + 408600 set correct jetty.url in all pom files + + 408642 setContentType from addHeader + + 408662 In pax-web servlet services requests even if init() has not finished + running + + 408806 getParameter returns null on Multipart request if called before + request.getPart()/getParts() + + 408909 GzipFilter setting of headers when reset and/or not compressed + + 409028 Jetty HttpClient does not work with proxy CONNECT method. + + 409133 Empty causes StackOverflowError + + 409436 NPE on context restart using dynamic servlet registration + + 409449 Ensure servlets, filters and listeners added via dynamic + registration, annotations or descriptors are cleaned on context restarts + + 409556 FileInputStream not closed in DirectNIOBuffer + + 410405 Avoid NPE for requestDispatcher(../) + + 410630 MongoSessionManager conflicting session update op + + 410750 NoSQLSessions: implement session context data persistence across + server restarts + + 410893 async support defaults to false for spec created servlets and filters + + 411135 HttpClient may send proxied https requests to the proxy instead of + the target server. + + 411216 RequestLogHandler handles async completion + + 411458 MultiPartFilter getParameterMap doesn't preserve multivalued + parameters 411459 MultiPartFilter.Wrapper getParameter should use charset + encoding of part + + 411755 MultiPartInputStreamParser fails on base64 encoded content + + 411909 GzipFilter flushbuffer() results in erroneous finish() call + + 412712 HttpClient does not send the terminal chunk after partial writes. + + 412750 HttpClient close expired connections fix + + 413371 Default JSON.Converters for List and Set. + + 413372 JSON Enum uses name rather than toString() + + 413684 Trailing slash shows JSP source + + 413812 Make RateTracker serializable + +jetty-7.6.12.v20130726 - 26 July 2013 + + 396706 CGI support parameters + + 397193 MongoSessionManager refresh updates last access time + + 407342 ReloadedSessionMissingClassTest uses class compiled with jdk7 + + 408529 Etags set in 304 response + + 408600 set correct jetty.url in all pom files + + 408642 setContentType from addHeader + + 408662 In pax-web servlet services requests even if init() has not finished + running + + 408909 GzipFilter setting of headers when reset and/or not compressed + + 409028 Jetty HttpClient does not work with proxy CONNECT method. + + 409133 Empty causes StackOverflowError + + 409556 FileInputStream not closed in DirectNIOBuffer + + 410630 MongoSessionManager conflicting session update op + + 410750 NoSQLSessions: implement session context data persistence across + server restarts + + 411135 HttpClient may send proxied https requests to the proxy instead of + the target server. + + 411216 RequestLogHandler handles async completion + + 411458 MultiPartFilter getParameterMap doesn't preserve multivalued + parameters 411459 MultiPartFilter.Wrapper getParameter should use charset + encoding of part + + 411755 MultiPartInputStreamParser fails on base64 encoded content + + 411909 GzipFilter flushbuffer() results in erroneous finish() call + + 412712 HttpClient does not send the terminal chunk after partial writes. + + 412750 HttpClient close expired connections fix + + 413371 Default JSON.Converters for List and Set. + + 413372 JSON Enum uses name rather than toString() + + 413684 Trailing slash shows JSP source + + 413812 Make RateTracker serializable + jetty-8.1.11.v20130520 - 20 May 2013 + 402844 STOP.PORT & STOP.KEY behaviour has changed + 403281 jetty.sh waits for started or failure before returning diff --git a/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModule.java b/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModule.java index 45561d4f422..028e4f269dc 100644 --- a/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModule.java +++ b/jetty-jaas/src/main/java/org/eclipse/jetty/jaas/spi/PropertyFileLoginModule.java @@ -20,10 +20,10 @@ package org.eclipse.jetty.jaas.spi; import java.security.Principal; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; @@ -45,7 +45,7 @@ public class PropertyFileLoginModule extends AbstractLoginModule private static final Logger LOG = Log.getLogger(PropertyFileLoginModule.class); - private static Map _propertyUserStores = new HashMap(); + private static ConcurrentHashMap _propertyUserStores = new ConcurrentHashMap(); private int _refreshInterval = 0; private String _filename = DEFAULT_FILENAME; @@ -68,33 +68,37 @@ public class PropertyFileLoginModule extends AbstractLoginModule private void setupPropertyUserStore(Map options) { + parseConfig(options); + if (_propertyUserStores.get(_filename) == null) { - parseConfig(options); + PropertyUserStore propertyUserStore = new PropertyUserStore(); + propertyUserStore.setConfig(_filename); + propertyUserStore.setRefreshInterval(_refreshInterval); - PropertyUserStore _propertyUserStore = new PropertyUserStore(); - _propertyUserStore.setConfig(_filename); - _propertyUserStore.setRefreshInterval(_refreshInterval); - LOG.debug("setupPropertyUserStore: Starting new PropertyUserStore. PropertiesFile: " + _filename + " refreshInterval: " + _refreshInterval); - - try + PropertyUserStore prev = _propertyUserStores.putIfAbsent(_filename, propertyUserStore); + if (prev == null) { - _propertyUserStore.start(); - } - catch (Exception e) - { - LOG.warn("Exception while starting propertyUserStore: ",e); - } + LOG.debug("setupPropertyUserStore: Starting new PropertyUserStore. PropertiesFile: " + _filename + " refreshInterval: " + _refreshInterval); - _propertyUserStores.put(_filename,_propertyUserStore); + try + { + propertyUserStore.start(); + } + catch (Exception e) + { + LOG.warn("Exception while starting propertyUserStore: ",e); + } + } } } private void parseConfig(Map options) { - _filename = (String)options.get("file") != null?(String)options.get("file"):DEFAULT_FILENAME; - String refreshIntervalString = (String)options.get("refreshInterval"); - _refreshInterval = refreshIntervalString == null?_refreshInterval:Integer.parseInt(refreshIntervalString); + String tmp = (String)options.get("file"); + _filename = (tmp == null? DEFAULT_FILENAME : tmp); + tmp = (String)options.get("refreshInterval"); + _refreshInterval = (tmp == null?_refreshInterval:Integer.parseInt(tmp)); } /** diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java index 7d70908618b..9098ea3bcf6 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java @@ -703,7 +703,6 @@ public class MultipartFilterTest assertTrue(response.getContent().contains("aaaa,bbbbb")); } - @Test public void testContentTypeWithCharSet() throws Exception { // generated and parsed test @@ -733,6 +732,39 @@ public class MultipartFilterTest assertTrue(response.getContent().indexOf("brown cow")>=0); } + + @Test + public void testBufferOverflowNoCRLF () throws Exception + { + String boundary="XyXyXy"; + // generated and parsed test + HttpTester.Request request = HttpTester.newRequest(); + HttpTester.Response response; + tester.addServlet(BoundaryServlet.class,"/testb"); + tester.setAttribute("fileName", "abc"); + tester.setAttribute("desc", "123"); + tester.setAttribute("title", "ttt"); + request.setMethod("POST"); + request.setVersion("HTTP/1.0"); + request.setHeader("Host","tester"); + request.setURI("/context/testb"); + request.setHeader("Content-Type","multipart/form-data; boundary="+boundary); + + String content = "--XyXyXy"; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(content.getBytes()); + + for (int i=0; i< 8500; i++) //create content that will overrun default buffer size of BufferedInputStream + { + baos.write('a'); + } + request.setContent(baos.toString()); + + response = HttpTester.parseResponse(tester.getResponses(request.generate())); + assertTrue(response.getContent().contains("Buffer size exceeded")); + assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, response.getStatus()); + } /* * see the testParameterMap test diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java index 5b9717d3119..ee29487d403 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java @@ -488,7 +488,16 @@ public class MultiPartInputStreamParser byte[] byteBoundary=(boundary+"--").getBytes(StringUtil.__ISO_8859_1); // Get first boundary - String line=((ReadLineInputStream)_in).readLine(); + String line = null; + try + { + line=((ReadLineInputStream)_in).readLine(); + } + catch (IOException e) + { + LOG.warn("Badly formatted multipart request"); + throw e; + } if (line == null) throw new IOException("Missing content for multipart request"); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java index bb4c9fbc877..1aeec5fbb60 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java @@ -49,6 +49,10 @@ public class ReadLineInputStream extends BufferedInputStream while (true) { int b=super.read(); + + if (markpos < 0) + throw new IOException("Buffer size exceeded: no line terminator"); + if (b==-1) { int m=markpos; diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java index b3f50cee0c1..e096b24e232 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/MultiPartInputStreamTest.java @@ -18,17 +18,8 @@ package org.eclipse.jetty.util; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -530,6 +521,34 @@ public class MultiPartInputStreamTest } @Test + public void testBufferOverflowNoCRLF () throws Exception + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write("--AaB03x".getBytes()); + for (int i=0; i< 8500; i++) //create content that will overrun default buffer size of BufferedInputStream + { + baos.write('a'); + } + + MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50); + MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(baos.toByteArray()), + _contentType, + config, + _tmpDir); + mpis.setDeleteOnExit(true); + try + { + mpis.getParts(); + fail ("Multipart buffer overrun"); + } + catch (IOException e) + { + assertTrue(e.getMessage().startsWith("Buffer size exceeded")); + } + + } + + public void testCharsetEncoding () throws Exception { String contentType = "multipart/form-data; boundary=TheBoundary; charset=ISO-8859-1"; diff --git a/pom.xml b/pom.xml index 45e04b39294..43202cae520 100644 --- a/pom.xml +++ b/pom.xml @@ -137,7 +137,7 @@ - ban-junit.jar + ban-junit-dep.jar enforce @@ -145,10 +145,10 @@ - junit:junit:*:jar + junit:junit-dep:*:jar true - We use junit-dep.jar, not junit.jar (as the standard junit.jar aggregates too many 3rd party libs inside of it) + We use junit.jar, not junit-dep.jar (as of junit 4.11, hamcrest is no longer embedded) @@ -553,7 +553,7 @@ org.eclipse.jetty.toolchain jetty-test-helper - 2.3 + 2.5 org.slf4j @@ -575,7 +575,7 @@ junit junit - 4.8.1 + 4.11 --> @@ -586,17 +586,17 @@ org.hamcrest hamcrest-core - 1.2.1 + 1.3 org.hamcrest hamcrest-library - 1.2.1 + 1.3 org.mockito mockito-core - 1.8.5 + 1.9.5 junit diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/JettyDistro.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/JettyDistro.java new file mode 100644 index 00000000000..99ce5ffdc29 --- /dev/null +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/support/JettyDistro.java @@ -0,0 +1,874 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 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.test.support; +// +//======================================================================== +//------------------------------------------------------------------------ +//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.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.IO; +import org.eclipse.jetty.toolchain.test.JAR; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.OS; +import org.eclipse.jetty.toolchain.test.PathAssert; +import org.eclipse.jetty.toolchain.test.TestingDir; +import org.junit.Assert; + +/** + * Basic process based executor for using the Jetty Distribution along with custom configurations to perform basic + *

+ * Allows for a test specific directory, that is a copied jetty-distribution, and then modified for the test specific testing required. + *

+ * Requires that you setup the maven-dependency-plugin appropriately for the base distribution you want to use, along with any other dependencies (wars, libs, + * etc..) that you may need from other maven projects. + *

+ * Maven Dependency Plugin Setup: + * + *

+ *  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ *    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ * 
+ *   <!-- Common Destination Directories -->
+ * 
+ *   <properties>
+ *     <test-wars-dir>${project.build.directory}/test-wars</test-wars-dir>
+ *     <test-libs-dir>${project.build.directory}/test-libs</test-libs-dir>
+ *     <test-distro-dir>${project.build.directory}/test-dist</test-distro-dir>
+ *   </properties>
+ * 
+ *   <build>
+ *     <plugins>
+ *       <plugin>
+ *         <groupId>org.apache.maven.plugins</groupId>
+ *         <artifactId>maven-dependency-plugin</artifactId>
+ *         <version>2.1</version>
+ *         <executions>
+ * 
+ *           <!-- Copy LIB and WAR dependencies into place that JettyDistro can use them -->
+ * 
+ *           <execution>
+ *             <id>test-lib-war-copy</id>
+ *             <phase>process-test-resources</phase>
+ *             <goals>
+ *               <goal>copy</goal>
+ *             </goals>
+ *             <configuration>
+ *               <artifactItems>
+ *                 <artifactItem>
+ *                   <groupId>org.mortbay.jetty.testwars</groupId>
+ *                   <artifactId>test-war-java_util_logging</artifactId>
+ *                   <version>7.3.0</version>
+ *                   <type>war</type>
+ *                   <outputDirectory>${test-wars-dir}</outputDirectory>
+ *                 </artifactItem>
+ *                 <artifactItem>
+ *                   <groupId>org.mortbay.jetty</groupId>
+ *                   <artifactId>jetty-aspect-servlet-api-2.5</artifactId>
+ *                   <version>7.3.0</version>
+ *                   <type>jar</type>
+ *                   <outputDirectory>${test-libs-dir}</outputDirectory>
+ *                 </artifactItem>
+ *               </artifactItems>
+ *               <overWriteIfNewer>true</overWriteIfNewer>
+ *               <overWrite>true</overWrite>
+ *               <stripVersion>true</stripVersion>
+ *             </configuration>
+ *           </execution>
+ * 
+ *           <!-- Extract Jetty DISTRIBUTION into place that JettyDistro can use it -->
+ * 
+ *           <execution>
+ *             <id>unpack-test-dist</id>
+ *             <phase>process-test-resources</phase>
+ *             <goals>
+ *               <goal>unpack</goal>
+ *             </goals>
+ *             <configuration>
+ *               <artifactItems>
+ *                 <artifactItem>
+ *                   <groupId>org.eclipse.jetty</groupId>
+ *                   <artifactId>jetty-distribution</artifactId>
+ *                   <version>7.3.0</version>
+ *                   <type>zip</type>
+ *                   <overWrite>true</overWrite>
+ *                 </artifactItem>
+ *               </artifactItems>
+ *               <outputAbsoluteArtifactFilename>true</outputAbsoluteArtifactFilename>
+ *               <outputDirectory>${test-distro-dir}</outputDirectory>
+ *               <overWriteSnapshots>true</overWriteSnapshots>
+ *               <overWriteIfNewer>true</overWriteIfNewer>
+ *             </configuration>
+ *           </execution>
+ *         </executions>
+ *       </plugin>
+ *     </plugins>
+ *   </build>
+ * 
+ * </project>
+ * 
+ *

+ * If you have a specific configuration you want to setup, you'll want to prepare this configuration in an overlay directory underneath the + * src/test/resources/ directory.
+ * Notes: + *

    + *
  1. The {@link JettyDistro} sets up a unique test directory (based on the constructor {@link #JettyDistro(Class)} or {@link #JettyDistro(TestingDir)}), by + * ensuring the directory is empty, then copying the target/test-dist directory into this new testing directory prior to the test specific changes + * to the configuration.
    + * Note: this testing directory is a complete jetty distribution, suitable for executing via the command line for additional testing needs.
  2. + *
  3. The directory name you choose in src/test/resources will be the name you use in the {@link #overlayConfig(String)} method to provide + * replacement configurations for the Jetty Distribution.
  4. + *
  5. You'll want to {@link #delete(String)} any files and/or directories from the standard distribution prior to using the {@link #overlayConfig(String)} + * method.
  6. + *
  7. Use the {@link #copyLib(String, String)} method to copy JAR files from the target/test-libs directory (created and managed above using the + * maven-dependency-plugin) to copy the lib into the test specific.
  8. + *
  9. Use the {@link #copyTestWar(String)} method to copy WAR files from the target/test-wars directory (created and managed above using the + * maven-dependency-plugin) to copy the WAR into the test specific directory.
  10. + *
+ *

+ * Next you'll want to use Junit 4.8+ and the @BeforeClass and @AfterClass annotations to setup the JettyDistro + * class for setting up your testing configuration. + *

+ * Example Test Case using {@link JettyDistro} class + * + *

+ * public class MySampleTest
+ * {
+ *     private static JettyDistro jetty;
+ * 
+ *     @BeforeClass
+ *     public static void initJetty() throws Exception
+ *     {
+ *         jetty = new JettyDistro(MySampleTest.class);
+ * 
+ *         jetty.copyTestWar("test-war-java_util_logging.war");
+ *         jetty.copyTestWar("test-war-policy.war");
+ * 
+ *         jetty.delete("webapps/test.war");
+ *         jetty.delete("contexts/test.d");
+ *         jetty.delete("contexts/javadoc.xml");
+ *         jetty.delete("contexts/test.xml");
+ * 
+ *         jetty.overlayConfig("no_security");
+ * 
+ *         jetty.setDebug(true);
+ * 
+ *         jetty.start();
+ *     }
+ * 
+ *     @AfterClass
+ *     public static void shutdownJetty() throws Exception
+ *     {
+ *         if (jetty != null)
+ *         {
+ *             jetty.stop();
+ *         }
+ *     }
+ * 
+ *     @Test
+ *     public void testRequest() throws Exception
+ *     {
+ *         SimpleRequest request = new SimpleRequest(jetty.getBaseUri());
+ *         String path = "/test-war-policy/security/PRACTICAL/testFilsystem");
+ *         String response = request.getString(path);
+ *         Assert.assertEquals("Success", response);
+ *     }
+ * }
+ * 
+ */ +public class JettyDistro +{ + private String artifactName = "jetty-distribution"; + private long startTime = 60; + private TimeUnit timeUnit = TimeUnit.SECONDS; + + private File jettyHomeDir; + private Process pid; + private URI baseUri; + + private String jmxUrl; + + private boolean _debug = false; + + /** + * Setup the JettyHome as belonging in a testing directory associated with a testing clazz. + * + * @param clazz + * the testing class using this JettyDistro + * @throws IOException + * if unable to copy unpacked distribution into place for the provided testing directory + */ + public JettyDistro(Class clazz) throws IOException + { + this(clazz,null); + } + + /** + * Setup the JettyHome as belonging in a testing directory associated with a testing clazz. + * + * @param clazz + * the testing class using this JettyDistro + * @param artifact + * name of jetty distribution artifact + * @throws IOException + * if unable to copy unpacked distribution into place for the provided testing directory + */ + public JettyDistro(Class clazz, String artifact) throws IOException + { + this.jettyHomeDir = MavenTestingUtils.getTargetTestingDir(clazz,"jettyHome"); + if (artifact != null) + { + this.artifactName = artifact; + } + + copyBaseDistro(); + } + + /** + * Setup the JettyHome as belonging to a specific testing method directory + * + * @param testdir + * the testing directory to use as the JettyHome for this JettyDistro + * @throws IOException + * if unable to copy unpacked distribution into place for the provided testing directory + */ + public JettyDistro(TestingDir testdir) throws IOException + { + this.jettyHomeDir = testdir.getDir(); + copyBaseDistro(); + } + + /** + * Setup the JettyHome as belonging to a specific testing method directory + * + * @param testdir + * the testing directory to use as the JettyHome for this JettyDistro + * @param artifact + * name of jetty distribution artifact + * @throws IOException + * if unable to copy unpacked distribution into place for the provided testing directory + */ + public JettyDistro(TestingDir testdir, String artifact) throws IOException + { + this.jettyHomeDir = testdir.getDir(); + if (artifact != null) + { + this.artifactName = artifact; + } + + copyBaseDistro(); + } + + /** + * + * @throws IOException + * if unable to copy unpacked distribution into place for the provided testing directory + */ + private void copyBaseDistro() throws IOException + { + // The outputDirectory for the maven side dependency:unpack goal. + File distroUnpackDir = MavenTestingUtils.getTargetFile("test-dist"); + PathAssert.assertDirExists(artifactName + " dependency:unpack",distroUnpackDir); + + // The actual jetty-distribution-${version} directory is under this directory. + // Lets find it. + File subdirs[] = distroUnpackDir.listFiles(new FileFilter() + { + public boolean accept(File path) + { + if (!path.isDirectory()) + { + return false; + } + + return path.getName().startsWith(artifactName + "-"); + } + }); + + if (subdirs.length == 0) + { + // No jetty-distribution found. + StringBuilder err = new StringBuilder(); + err.append("No target/test-dist/"); + err.append(artifactName); + err.append("-${version} directory found."); + err.append("\n To fix this, run 'mvn process-test-resources' to create the directory."); + throw new IOException(err.toString()); + } + + if (subdirs.length != 1) + { + // Too many jetty-distributions found. + StringBuilder err = new StringBuilder(); + err.append("Too many target/test-dist/"); + err.append(artifactName); + err.append("-${version} directories found."); + for (File dir : subdirs) + { + err.append("\n ").append(dir.getAbsolutePath()); + } + err.append("\n To fix this, run 'mvn clean process-test-resources' to recreate the target/test-dist directory."); + throw new IOException(err.toString()); + } + + File distroSrcDir = subdirs[0]; + FS.ensureEmpty(jettyHomeDir); + System.out.printf("Copying Jetty Distribution: %s%n",distroSrcDir.getAbsolutePath()); + System.out.printf(" To Testing Dir: %s%n",jettyHomeDir.getAbsolutePath()); + IO.copyDir(distroSrcDir,jettyHomeDir); + } + + /** + * Return the $(jetty.home) directory being used for this JettyDistro + * + * @return the jetty.home directory being used + */ + public File getJettyHomeDir() + { + return this.jettyHomeDir; + } + + /** + * Copy a war file from ${project.basedir}/target/test-wars/${testWarFilename} into the ${jetty.home}/webapps/ directory + * + * @param testWarFilename + * the war file to copy (must exist) + * @throws IOException + * if unable to copy the war file. + */ + public void copyTestWar(String testWarFilename) throws IOException + { + File srcWar = MavenTestingUtils.getTargetFile("test-wars/" + testWarFilename); + File destWar = new File(jettyHomeDir,OS.separators("webapps/" + testWarFilename)); + FS.ensureDirExists(destWar.getParentFile()); + IO.copyFile(srcWar,destWar); + } + + /** + * Copy an arbitrary file from src/test/resources/${resourcePath} to the testing directory. + * + * @param resourcePath + * the relative path for file content within the src/test/resources directory. + * @param outputPath + * the testing directory relative output path for the file output (will result in a file with the outputPath name being created) + * @throws IOException + * if unable to copy resource file + */ + public void copyResource(String resourcePath, String outputPath) throws IOException + { + File srcFile = MavenTestingUtils.getTestResourceFile(resourcePath); + File destFile = new File(jettyHomeDir,OS.separators(outputPath)); + FS.ensureDirExists(destFile.getParentFile()); + IO.copyFile(srcFile,destFile); + } + + /** + * Copy an arbitrary file from target/test-libs/${libFilename} to the testing directory. + * + * @param libFilename + * the target/test-libs/${libFilename} to copy + * @param outputPath + * the destination testing directory relative output path for the lib. (will result in a file with the outputPath name being created) + * @throws IOException + * if unable to copy lib + */ + public void copyLib(String libFilename, String outputPath) throws IOException + { + File srcLib = MavenTestingUtils.getTargetFile("test-libs/" + libFilename); + File destLib = new File(jettyHomeDir,OS.separators(outputPath)); + FS.ensureDirExists(destLib.getParentFile()); + IO.copyFile(srcLib,destLib); + } + + /** + * Copy the ${project.basedir}/src/main/config/ tree into the testing directory. + * + * @throws IOException + * if unable to copy the directory tree + */ + public void copyProjectMainConfig() throws IOException + { + File srcDir = MavenTestingUtils.getProjectDir("src/main/config"); + IO.copyDir(srcDir,jettyHomeDir); + } + + /** + * Create a ${jetty.home}/lib/self/${jarFilename} jar file from the content in the ${project.basedir}/target/classes/ directory. + * + * @throws IOException + * if unable to copy the directory tree + */ + public void createProjectLib(String jarFilename) throws IOException + { + File srcDir = MavenTestingUtils.getTargetFile("classes"); + File libSelfDir = new File(jettyHomeDir,OS.separators("lib/self")); + FS.ensureDirExists(libSelfDir); + File jarFile = new File(libSelfDir,jarFilename); + JAR.create(srcDir,jarFile); + } + + /** + * Unpack an arbitrary config from target/test-configs/${configFilename} to the testing directory. + * + * @param configFilename + * the target/test-configs/${configFilename} to copy + * @throws IOException + * if unable to unpack config file + */ + public void unpackConfig(String configFilename) throws IOException + { + File srcConfig = MavenTestingUtils.getTargetFile("test-configs/" + configFilename); + JAR.unpack(srcConfig,jettyHomeDir); + } + + /** + * Delete a File or Directory found in the ${jetty.home} directory. + * + * @param path + * the path to delete. (can be a file or directory) + */ + public void delete(String path) + { + File jettyPath = new File(jettyHomeDir,OS.separators(path)); + FS.delete(jettyPath); + } + + /** + * Return the baseUri being used for this Jetty Process Instance. + * + * @return the base URI for this Jetty Process Instance. + */ + public URI getBaseUri() + { + return this.baseUri; + } + + /** + * Return the JMX URL being used for this Jetty Process Instance. + * + * @return the JMX URL for this Jetty Process Instance. + */ + public String getJmxUrl() + { + return this.jmxUrl; + } + + /** + * Take the directory contents from ${project.basedir}/src/test/resources/${testConfigName}/ and copy it over whatever happens to be at ${jetty.home} + * + * @param testConfigName + * the src/test/resources/ directory name to use as the source diretory for the configuration we are interested in. + * @throws IOException + * if unable to copy directory. + */ + public void overlayConfig(String testConfigName) throws IOException + { + File srcDir = MavenTestingUtils.getTestResourceDir(testConfigName); + IO.copyDir(srcDir,jettyHomeDir); + } + + /** + * Start the jetty server + * + * @throws IOException + * if unable to start the server. + */ + public void start() throws IOException + { + List commands = new ArrayList(); + commands.add(getJavaBin()); + + commands.add("-Djetty.home=" + jettyHomeDir.getAbsolutePath()); + + // Do a dry run first to get the exact command line for Jetty process + commands.add("-jar"); + commands.add("start.jar"); + commands.add("jetty.port=0"); + if (_debug) + { + commands.add("-D.DEBUG=true"); + } + commands.add("--dry-run"); + + ProcessBuilder pbCmd = new ProcessBuilder(commands); + pbCmd.directory(jettyHomeDir); + + String cmdLine = null; + Process pidCmd = pbCmd.start(); + try + { + cmdLine = readOutputLine(pidCmd); + } + finally + { + pidCmd.destroy(); + } + + if (cmdLine == null || !cmdLine.contains("XmlConfiguration")) + { + Assert.fail("Unable to get Jetty command line"); + } + + // Need to breakdown commandline into parts, as spaces in command line will cause failures. + List execCommands = splitAndUnescapeCommandLine(cmdLine); + + System.out.printf("Executing: %s%n",cmdLine); + System.out.printf("Working Dir: %s%n",jettyHomeDir.getAbsolutePath()); + + pbCmd = new ProcessBuilder(execCommands); + pid = pbCmd.start(); + + ConsoleParser parser = new ConsoleParser(); + List jmxList = parser.newPattern("JMX Remote URL: (.*)",0); + List connList = parser.newPattern("Started [A-Za-z]*Connector@([0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*):([0-9]*)",1); + // DISABLED: This is what exists in Jetty 9+ + // List connList = parser.newPattern("Started [A-Za-z]*Connector@.*[\\({]([0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*):([0-9]*)[\\)}].*",1); + + startPump("STDOUT",parser,this.pid.getInputStream()); + startPump("STDERR",parser,this.pid.getErrorStream()); + + try + { + parser.waitForDone(this.startTime,this.timeUnit); + + if (!jmxList.isEmpty()) + { + this.jmxUrl = jmxList.get(0)[0]; + System.out.printf("## Found JMX connector at %s%n",this.jmxUrl); + } + + if (!connList.isEmpty()) + { + String[] params = connList.get(0); + if (params.length == 2) + { + this.baseUri = URI.create("http://localhost:" + params[1] + "/"); + } + System.out.printf("## Found Jetty connector at host: %s port: %s%n",(Object[])params); + } + + } + catch (InterruptedException e) + { + pid.destroy(); + Assert.fail("Unable to get required information within time limit"); + } + } + + public static List splitAndUnescapeCommandLine(CharSequence rawCmdLine) + { + List cmds = new ArrayList(); + + int len = rawCmdLine.length(); + StringBuilder arg = new StringBuilder(); + boolean escaped = false; + boolean inQuote = false; + char c; + for (int i = 0; i < len; i++) + { + c = rawCmdLine.charAt(i); + if (escaped) + { + switch (c) + { + case 'r': + arg.append('\r'); + break; + case 'f': + arg.append('\f'); + break; + case 't': + arg.append('\t'); + break; + case 'n': + arg.append('\n'); + break; + case 'b': + arg.append('\b'); + break; + default: + arg.append(c); + break; + } + escaped = false; + continue; + } + + if (c == '\\') + { + escaped = true; + } + else + { + if ((c == ' ') && (!inQuote)) + { + // the delim! + cmds.add(String.valueOf(arg.toString())); + arg.setLength(0); + } + else if (c == '"') + { + inQuote = !inQuote; + } + else + { + arg.append(c); + } + } + } + cmds.add(String.valueOf(arg.toString())); + + return cmds; + } + + private String readOutputLine(Process pidCmd) throws IOException + { + InputStream in = null; + InputStreamReader reader = null; + BufferedReader buf = null; + try + { + in = pidCmd.getInputStream(); + reader = new InputStreamReader(in); + buf = new BufferedReader(reader); + return buf.readLine(); + } + finally + { + IO.close(buf); + IO.close(reader); + IO.close(in); + } + } + + private static class ConsoleParser + { + private List patterns = new ArrayList(); + private CountDownLatch latch; + private int count; + + public List newPattern(String exp, int cnt) + { + ConsolePattern pat = new ConsolePattern(exp,cnt); + patterns.add(pat); + count += cnt; + + return pat.getMatches(); + } + + public void parse(String line) + { + for (ConsolePattern pat : patterns) + { + Matcher mat = pat.getMatcher(line); + if (mat.find()) + { + int num = 0, count = mat.groupCount(); + String[] match = new String[count]; + while (num++ < count) + { + match[num - 1] = mat.group(num); + } + pat.getMatches().add(match); + + if (pat.getCount() > 0) + { + getLatch().countDown(); + } + } + } + } + + public void waitForDone(long timeout, TimeUnit unit) throws InterruptedException + { + getLatch().await(timeout,unit); + } + + private CountDownLatch getLatch() + { + synchronized (this) + { + if (latch == null) + { + latch = new CountDownLatch(count); + } + } + + return latch; + } + } + + private static class ConsolePattern + { + private Pattern pattern; + private List matches; + private int count; + + ConsolePattern(String exp, int cnt) + { + pattern = Pattern.compile(exp); + matches = new ArrayList(); + count = cnt; + } + + public Matcher getMatcher(String line) + { + return pattern.matcher(line); + } + + public List getMatches() + { + return matches; + } + + public int getCount() + { + return count; + } + } + + private void startPump(String mode, ConsoleParser parser, InputStream inputStream) + { + ConsoleStreamer pump = new ConsoleStreamer(mode,inputStream); + pump.setParser(parser); + Thread thread = new Thread(pump,"ConsoleStreamer/" + mode); + thread.start(); + } + + /** + * enable debug on the jetty process + * + * @param debug + */ + public void setDebug(boolean debug) + { + _debug = debug; + } + + private String getJavaBin() + { + String javaexes[] = new String[] + { "java", "java.exe" }; + + File javaHomeDir = new File(System.getProperty("java.home")); + for (String javaexe : javaexes) + { + File javabin = new File(javaHomeDir,OS.separators("bin/" + javaexe)); + if (javabin.exists() && javabin.isFile()) + { + return javabin.getAbsolutePath(); + } + } + + Assert.fail("Unable to find java bin"); + return "java"; + } + + /** + * Stop the jetty server + */ + public void stop() + { + System.out.println("Stopping JettyDistro ..."); + if (pid != null) + { + // TODO: maybe issue a STOP instead? + pid.destroy(); + } + } + + /** + * Simple streamer for the console output from a Process + */ + private static class ConsoleStreamer implements Runnable + { + private String mode; + private BufferedReader reader; + private ConsoleParser parser; + + public ConsoleStreamer(String mode, InputStream is) + { + this.mode = mode; + this.reader = new BufferedReader(new InputStreamReader(is)); + } + + public void setParser(ConsoleParser connector) + { + this.parser = connector; + } + + public void run() + { + String line; + // System.out.printf("ConsoleStreamer/%s initiated%n",mode); + try + { + while ((line = reader.readLine()) != (null)) + { + if (parser != null) + { + parser.parse(line); + } + System.out.println("[" + mode + "] " + line); + } + } + catch (IOException ignore) + { + /* ignore */ + } + finally + { + IO.close(reader); + } + // System.out.printf("ConsoleStreamer/%s finished%n",mode); + } + } + + public void setStartTime(long startTime, TimeUnit timeUnit) + { + this.startTime = startTime; + this.timeUnit = timeUnit; + } +} \ No newline at end of file