From af5f8aac33414c7a1713e818f4b72cc0576a2eba Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Thu, 28 Mar 2013 15:01:59 +1100 Subject: [PATCH] 403122 Session replication fails with ClassNotFoundException when session attribute is Java dynamic proxy --- .../server/session/HashSessionManager.java | 35 +---- .../server/session/JDBCSessionManager.java | 37 +---- .../util/ClassLoadingObjectInputStream.java | 105 +++++++++++++ .../session/ProxySerializationTest.java | 78 ++++++++++ .../session/ProxySerializationTest.java | 58 +++++++ .../AbstractProxySerializationTest.java | 146 ++++++++++++++++++ .../main/resources/proxy-serialization.jar | Bin 0 -> 5378 bytes 7 files changed, 389 insertions(+), 70 deletions(-) create mode 100644 jetty-util/src/main/java/org/eclipse/jetty/util/ClassLoadingObjectInputStream.java create mode 100644 tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java create mode 100644 tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java create mode 100644 tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractProxySerializationTest.java create mode 100644 tests/test-sessions/test-sessions-common/src/main/resources/proxy-serialization.jar diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionManager.java index b75649479de..1954894aafe 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionManager.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionManager.java @@ -24,7 +24,6 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.ObjectInputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; @@ -37,6 +36,7 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.ClassLoadingObjectInputStream; import org.eclipse.jetty.util.log.Logger; @@ -641,37 +641,4 @@ public class HashSessionManager extends AbstractSessionManager } } } - - - - /* ------------------------------------------------------------ */ - /* ------------------------------------------------------------ */ - protected class ClassLoadingObjectInputStream extends ObjectInputStream - { - /* ------------------------------------------------------------ */ - public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException - { - super(in); - } - - /* ------------------------------------------------------------ */ - public ClassLoadingObjectInputStream () throws IOException - { - super(); - } - - /* ------------------------------------------------------------ */ - @Override - public Class resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException - { - try - { - return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader()); - } - catch (ClassNotFoundException e) - { - return super.resolveClass(cl); - } - } - } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java index b4677fa3dd4..afe7ae2c0ae 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java @@ -29,12 +29,8 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -46,6 +42,7 @@ import javax.servlet.http.HttpSessionListener; import org.eclipse.jetty.server.SessionIdManager; import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.ClassLoadingObjectInputStream; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -387,38 +384,6 @@ public class JDBCSessionManager extends AbstractSessionManager - /** - * ClassLoadingObjectInputStream - * - * Used to persist the session attribute map - */ - protected class ClassLoadingObjectInputStream extends ObjectInputStream - { - public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException - { - super(in); - } - - public ClassLoadingObjectInputStream () throws IOException - { - super(); - } - - @Override - public Class resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException - { - try - { - return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader()); - } - catch (ClassNotFoundException e) - { - return super.resolveClass(cl); - } - } - } - - /** * Set the time in seconds which is the interval between * saving the session access time to the database. diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ClassLoadingObjectInputStream.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ClassLoadingObjectInputStream.java new file mode 100644 index 00000000000..5020795ad1f --- /dev/null +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ClassLoadingObjectInputStream.java @@ -0,0 +1,105 @@ +// +// ======================================================================== +// 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.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; + + +/** + * ClassLoadingObjectInputStream + * + * For re-inflating serialized objects, this class uses the thread context classloader + * rather than the jvm's default classloader selection. + * + */ +public class ClassLoadingObjectInputStream extends ObjectInputStream +{ + /* ------------------------------------------------------------ */ + public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException + { + super(in); + } + + /* ------------------------------------------------------------ */ + public ClassLoadingObjectInputStream () throws IOException + { + super(); + } + + /* ------------------------------------------------------------ */ + @Override + public Class resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException + { + try + { + return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader()); + } + catch (ClassNotFoundException e) + { + return super.resolveClass(cl); + } + } + + /* ------------------------------------------------------------ */ + @Override + protected Class resolveProxyClass(String[] interfaces) + throws IOException, ClassNotFoundException + { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + + ClassLoader nonPublicLoader = null; + boolean hasNonPublicInterface = false; + + // define proxy in class loader of non-public interface(s), if any + Class[] classObjs = new Class[interfaces.length]; + for (int i = 0; i < interfaces.length; i++) + { + Class cl = Class.forName(interfaces[i], false, loader); + if ((cl.getModifiers() & Modifier.PUBLIC) == 0) + { + if (hasNonPublicInterface) + { + if (nonPublicLoader != cl.getClassLoader()) + { + throw new IllegalAccessError( + "conflicting non-public interface class loaders"); + } + } + else + { + nonPublicLoader = cl.getClassLoader(); + hasNonPublicInterface = true; + } + } + classObjs[i] = cl; + } + try + { + return Proxy.getProxyClass(hasNonPublicInterface ? nonPublicLoader : loader,classObjs); + } + catch (IllegalArgumentException e) + { + throw new ClassNotFoundException(null, e); + } + } +} \ No newline at end of file diff --git a/tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java b/tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java new file mode 100644 index 00000000000..e8101e7d2e7 --- /dev/null +++ b/tests/test-sessions/test-hash-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java @@ -0,0 +1,78 @@ +// +// ======================================================================== +// 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.server.session; + +import java.io.File; + +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.junit.Test; + +/** + * ProxySerializationTest + * + * + */ +public class ProxySerializationTest extends AbstractProxySerializationTest +{ + /** + * @see org.eclipse.jetty.server.session.AbstractProxySerializationTest#createServer(int, int, int) + */ + @Override + public AbstractTestServer createServer(int port, int max, int scavenge) + { + return new HashTestServer(port,max,scavenge); + } + + + + + @Override + public void customizeContext(ServletContextHandler c) + { + if (c == null) + return; + + //Ensure that the HashSessionManager will persist sessions on passivation + HashSessionManager manager = (HashSessionManager)c.getSessionHandler().getSessionManager(); + manager.setLazyLoad(false); + manager.setIdleSavePeriod(1); + try + { + File testDir = MavenTestingUtils.getTargetTestingDir("foo"); + testDir.mkdirs(); + manager.setStoreDirectory(testDir); + } + catch (Exception e) + { + throw new IllegalStateException(e); + } + } + + + + + @Test + public void testProxySerialization() throws Exception + { + super.testProxySerialization(); + } + +} diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java new file mode 100644 index 00000000000..3b54cad385c --- /dev/null +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ProxySerializationTest.java @@ -0,0 +1,58 @@ +// +// ======================================================================== +// 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.server.session; + +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.junit.Test; + +/** + * ProxySerializationTest + * + * + */ +public class ProxySerializationTest extends AbstractProxySerializationTest +{ + + /** + * @see org.eclipse.jetty.server.session.AbstractProxySerializationTest#createServer(int, int, int) + */ + @Override + public AbstractTestServer createServer(int port, int max, int scavenge) + { + return new JdbcTestServer(port, max, scavenge); + } + + /** + * @see org.eclipse.jetty.server.session.AbstractProxySerializationTest#customizeContext(org.eclipse.jetty.servlet.ServletContextHandler) + */ + @Override + public void customizeContext(ServletContextHandler c) + { + } + + + + @Test + public void testProxySerialization() throws Exception + { + super.testProxySerialization(); + } + +} diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractProxySerializationTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractProxySerializationTest.java new file mode 100644 index 00000000000..e7fe83f1f52 --- /dev/null +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractProxySerializationTest.java @@ -0,0 +1,146 @@ +// +// ======================================================================== +// 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.server.session; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Proxy; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.jar.JarFile; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.resource.JarResource; +import org.junit.Test; + +/** + * AbstractProxySerializationTest + * + * + */ +public abstract class AbstractProxySerializationTest +{ + public abstract AbstractTestServer createServer(int port, int max, int scavenge); + + public abstract void customizeContext (ServletContextHandler c); + + + + /** + * @param sec mseconds to sleep + */ + public void pause(int msec) + { + try + { + Thread.sleep(msec); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + + @Test + public void testProxySerialization() throws Exception + { + String contextPath = ""; + String servletMapping = "/server"; + int scavengePeriod = 10; + AbstractTestServer server = createServer(0, 20, scavengePeriod); + ServletContextHandler context = server.addContext(contextPath); + + InputStream is = this.getClass().getClassLoader().getResourceAsStream("proxy-serialization.jar"); + + File testDir = MavenTestingUtils.getTargetTestingDir("proxy-serialization"); + testDir.mkdirs(); + + File extractedJar = new File (testDir, "proxy-serialization.jar"); + extractedJar.createNewFile(); + IO.copy(is, new FileOutputStream(extractedJar)); + + + URLClassLoader loader = new URLClassLoader(new URL[] {extractedJar.toURI().toURL()}, Thread.currentThread().getContextClassLoader()); + context.setClassLoader(loader); + context.addServlet("TestServlet", servletMapping); + customizeContext(context); + + try + { + server.start(); + int port=server.getPort(); + HttpClient client = new HttpClient(); + client.start(); + try + { + ContentResponse response = client.GET("http://localhost:" + port + contextPath + servletMapping + "?action=create"); + assertEquals(HttpServletResponse.SC_OK,response.getStatus()); + String sessionCookie = response.getHeaders().getStringField("Set-Cookie"); + assertTrue(sessionCookie != null); + // Mangle the cookie, replacing Path with $Path, etc. + sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); + + //stop the context to be sure the sesssion will be passivated + context.stop(); + + //after a stop some of the volatile info is lost, so reinstate it + context.setClassLoader(loader); + context.addServlet("TestServlet", servletMapping); + + //restart the context + context.start(); + + // Make another request using the session id from before + Request request = client.newRequest("http://localhost:" + port + contextPath + servletMapping + "?action=test"); + request.header("Cookie", sessionCookie); + response = request.send(); + assertEquals(HttpServletResponse.SC_OK,response.getStatus()); + } + finally + { + client.stop(); + } + } + finally + { + server.stop(); + } + + } +} diff --git a/tests/test-sessions/test-sessions-common/src/main/resources/proxy-serialization.jar b/tests/test-sessions/test-sessions-common/src/main/resources/proxy-serialization.jar new file mode 100644 index 0000000000000000000000000000000000000000..fe3f0402dd9c38d01944d5a587abc8be4cbe7211 GIT binary patch literal 5378 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 a+JEl)KiZ`DRsjH@#Qz-eF&08kc>5o+{ppDS literal 0 HcmV?d00001