diff options
-rw-r--r-- | luni/src/test/java/libcore/java/net/URLTest.java | 179 | ||||
-rw-r--r-- | luni/src/test/java/libcore/libcore/util/SerializationTester.java | 2 | ||||
-rw-r--r-- | ojluni/src/main/java/java/net/URL.java | 315 |
3 files changed, 461 insertions, 35 deletions
diff --git a/luni/src/test/java/libcore/java/net/URLTest.java b/luni/src/test/java/libcore/java/net/URLTest.java index 4bae646e65..1ff9b336cb 100644 --- a/luni/src/test/java/libcore/java/net/URLTest.java +++ b/luni/src/test/java/libcore/java/net/URLTest.java @@ -16,14 +16,28 @@ package libcore.java.net; +import junit.framework.TestCase; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.net.Inet6Address; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; +import libcore.libcore.util.SerializationTester; import dalvik.system.BlockGuard; -import junit.framework.TestCase; -import libcore.libcore.util.SerializationTester; + +import static java.util.Arrays.asList; public final class URLTest extends TestCase { @@ -109,6 +123,149 @@ public final class URLTest extends TestCase { } /** + * For a custom URLStreamHandler, a (de)serialization round trip reconstructs an + * inconsistently null authority from host and port. + */ + public void testUrlSerializationRoundTrip_customHandler_nullAuthorityReconstructed() + throws Exception { + withCustomURLStreamHandlerFactory(() -> { + URL url = new URL("android://example.com:80/file"); + getUrlField("authority").set(url, null); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + + assertFields(url, "android", "example.com", 80, null, "/file"); + assertFields(reserializedUrl, "android", "example.com", 80, "example.com:80", "/file"); + return null; + }); + } + + /** + * For a custom URLStreamHandler, a (de)serialization round trip does not reconstruct + * an inconsistent but nonnull authority from host and port. + */ + public void testUrlSerializationRoundTrip_customHandler_nonnullAuthorityNotReconstructed() + throws Exception { + withCustomURLStreamHandlerFactory(() -> { + URL url = new URL("android://example.com/file"); + getUrlField("authority").set(url, "evil.com:1234"); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + + assertFields(url, "android", "example.com", -1, "evil.com:1234", "/file"); + assertFields(reserializedUrl, "android", "example.com", -1, "evil.com:1234", "/file"); + return null; + }); + } + + /** + * For a custom URLStreamHandler, a (de)serialization round trip does not + * reconstruct host and port from the authority, even if host is null. + */ + public void testUrlSerializationRoundTrip_customHandler_hostAndPortNotReconstructed() + throws Exception { + checkUrlSerializationRoundTrip_customHandler_hostAndPortNotReconstructed(null /* host */); + checkUrlSerializationRoundTrip_customHandler_hostAndPortNotReconstructed("evil.com"); + } + + private void checkUrlSerializationRoundTrip_customHandler_hostAndPortNotReconstructed( + final String hostOrNull) throws Exception { + withCustomURLStreamHandlerFactory(() -> { + URL url = new URL("android://example.com:80/file"); + getUrlField("host").set(url, hostOrNull); + getUrlField("port").set(url, 12345); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + + assertFields(url, "android", hostOrNull, 12345, "example.com:80", "/file"); + assertFields(reserializedUrl, "android", hostOrNull, 12345, "example.com:80", "/file"); + return null; + }); + } + + /** + * Temporarily registers a {@link URLStreamHandlerFactory} that accepts any protocol, + * and then, while that factory is registered, runs the given {@code callable} on this + * thread. + * @throw Exception any Exception thrown by the Callable will be thrown on to the caller. + */ + private static void withCustomURLStreamHandlerFactory(Callable<Void> callable) + throws Exception { + Field factoryField = getUrlField("factory"); + assertTrue(Modifier.isStatic(factoryField.getModifiers())); + URLStreamHandlerFactory oldFactory = (URLStreamHandlerFactory) factoryField.get(null); + try { + URL.setURLStreamHandlerFactory( + protocol -> new libcore.java.net.customstreamhandler.http.Handler()); + callable.call(); + } finally { + factoryField.set(null, null); + URL.setURLStreamHandlerFactory(oldFactory); + } + } + + /** + * Host and port are reconstructed from the authority during deserialization. + */ + public void testUrlSerializationRoundTrip_builtinHandler_hostAndPortReconstructed() + throws Exception { + URL url = new URL("http://example.com:42/file"); + getUrlField("host").set(url, "wronghost.com"); + getUrlField("port").setInt(url, 1234); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + assertFields(url, "http", "wronghost.com", 1234, "example.com:42", "/file"); + assertFields(reserializedUrl, "http", "example.com", 42, "example.com:42", "/file"); + + // Check that the normalization occurs during deserialization rather than during + // serialization. + assertFalse(Arrays.equals( + SerializationTester.serialize(url), + SerializationTester.serialize(reserializedUrl) + )); + assertTrue(Arrays.equals( + SerializationTester.serialize(reserializedUrl), + SerializationTester.serialize(reserializedUrl) + )); + } + + /** + * The authority is not reconstructed from host and port, but the other way around. + */ + public void testUrlSerializationRoundTrip_builtinHandler_authorityNotReconstructed() + throws Exception { + URL url = new URL("http://example.com/file"); + getUrlField("authority").set(url, "newhost.com:80"); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + + assertFields(url, "http", "example.com", -1, "newhost.com:80", "/file"); + assertFields(reserializedUrl, "http", "newhost.com", 80, "newhost.com:80", "/file"); + } + + /** + * The boundary where the authority part ends and the file part starts is + * reconstructed during deserialization. + */ + public void testUrlSerializationRoundTrip_builtinHandler_authorityAndFileReconstructed() + throws Exception { + URL url = new URL("http://temporaryhost.com/temporaryfile"); + getUrlField("authority").set(url, "exam"); + getUrlField("file").set(url, "ple.com:80/file"); + URL reserializedUrl = (URL) SerializationTester.reserialize(url); + assertFields(reserializedUrl, "http", "example.com", 80, "example.com:80", "/file"); + } + + private static Field getUrlField(String fieldName) throws Exception { + Field result = URL.class.getDeclaredField(fieldName); + result.setAccessible(true); + return result; + } + + private static void assertFields(URL url, + String protocol, String host, int port, String authority, String file) { + assertEquals( + asList(protocol, host, port, authority, file), + asList(url.getProtocol(), url.getHost(), url.getPort(), url.getAuthority(), + url.getFile())); + } + + /** * The serialized form of a URL includes its hash code. But the hash code * is not documented. Check that we don't return a deserialized hash code * from a deserialized value. @@ -293,6 +450,24 @@ public final class URLTest extends TestCase { assertEquals("query@at", url.getQuery()); } + public void testCommonProtocolsAreHandledByBuiltinHandlers() throws Exception { + Method getURLStreamHandler = URL.class.getDeclaredMethod( + "getURLStreamHandler", String.class); + getURLStreamHandler.setAccessible(true); + Set<String> builtinHandlers = + (Set<String>) getUrlField("BUILTIN_HANDLER_CLASS_NAMES").get(null); + Set<String> commonHandlers = new HashSet<>(); + for (String protocol : Arrays.asList("file", "ftp", "jar", "http", "https")) { + URLStreamHandler handler = + (URLStreamHandler) getURLStreamHandler.invoke(null, protocol); + assertNotNull("Handler for protocol " + protocol + " should exist", handler); + commonHandlers.add(handler.getClass().getName()); + } + assertTrue("Built-in handlers " + builtinHandlers + " should contain all of the handlers " + + commonHandlers + " for common protocols.", + builtinHandlers.containsAll(commonHandlers)); + } + public void testColonInQuery() throws Exception { URL url = new URL("http://host/file?query:colon"); assertEquals("/file?query:colon", url.getFile()); diff --git a/luni/src/test/java/libcore/libcore/util/SerializationTester.java b/luni/src/test/java/libcore/libcore/util/SerializationTester.java index d30baf52cf..48a58b4fd0 100644 --- a/luni/src/test/java/libcore/libcore/util/SerializationTester.java +++ b/luni/src/test/java/libcore/libcore/util/SerializationTester.java @@ -81,7 +81,7 @@ public class SerializationTester<T> { } } - private static byte[] serialize(Object object) throws IOException { + public static byte[] serialize(Object object) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); new ObjectOutputStream(out).writeObject(object); return out.toByteArray(); diff --git a/ojluni/src/main/java/java/net/URL.java b/ojluni/src/main/java/java/net/URL.java index 495cc190ee..74dec92246 100644 --- a/ojluni/src/main/java/java/net/URL.java +++ b/ojluni/src/main/java/java/net/URL.java @@ -1,6 +1,6 @@ /* * Copyright (C) 2014 The Android Open Source Project - * Copyright (c) 1995, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -28,7 +28,14 @@ package java.net; import java.io.IOException; import java.io.InputStream; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream.GetField; +import java.io.ObjectStreamException; +import java.io.ObjectStreamField; +import java.util.Collections; +import java.util.HashSet; import java.util.Hashtable; +import java.util.Set; import java.util.StringTokenizer; import sun.security.util.SecurityConstants; @@ -136,6 +143,9 @@ import sun.security.util.SecurityConstants; */ public final class URL implements java.io.Serializable { + // Android-changed: Custom built-in URLStreamHandlers for http, https. + // static final String BUILTIN_HANDLERS_PREFIX = "sun.net.www.protocol"; + private static final Set<String> BUILTIN_HANDLER_CLASS_NAMES = createBuiltinHandlerClassNames(); static final long serialVersionUID = -7627629688361524110L; /** @@ -218,9 +228,9 @@ public final class URL implements java.io.Serializable { /* Our hash code. * @serial */ - // Android-changed: App compat. The cache of hash code should not be serialized. - //private int hashCode = -1; - private transient int hashCode = -1; + private int hashCode = -1; + + private transient UrlDeserializedState tempState; /** * Creates a {@code URL} object from the specified @@ -1178,7 +1188,9 @@ public final class URL implements java.io.Serializable { packagePrefixList += "sun.net.www.protocol"; */ final String packagePrefixList = System.getProperty(protocolPathProp,""); - StringTokenizer packagePrefixIter = new StringTokenizer(packagePrefixList, "|"); + + StringTokenizer packagePrefixIter = + new StringTokenizer(packagePrefixList, "|"); while (handler == null && packagePrefixIter.hasMoreTokens()) { @@ -1210,31 +1222,16 @@ public final class URL implements java.io.Serializable { } } - // BEGIN Android-added: Makes okhttp the default http/https handler. + // BEGIN Android-added: Custom built-in URLStreamHandlers for http, https. // Fallback to built-in stream handler. if (handler == null) { try { - // Use of okhttp for http and https - // Removed unnecessary use of reflection for sun classes - if (protocol.equals("file")) { - handler = new sun.net.www.protocol.file.Handler(); - } else if (protocol.equals("ftp")) { - handler = new sun.net.www.protocol.ftp.Handler(); - } else if (protocol.equals("jar")) { - handler = new sun.net.www.protocol.jar.Handler(); - } else if (protocol.equals("http")) { - handler = (URLStreamHandler)Class. - forName("com.android.okhttp.HttpHandler").newInstance(); - } else if (protocol.equals("https")) { - handler = (URLStreamHandler)Class. - forName("com.android.okhttp.HttpsHandler").newInstance(); - } - // END Android-changed + handler = createBuiltinHandler(protocol); } catch (Exception e) { throw new AssertionError(e); } } - // END Android-added: Makes okhttp the default http/https handler. + // END Android-added: Custom built-in URLStreamHandlers for http, https. synchronized (streamHandlerLock) { @@ -1273,6 +1270,69 @@ public final class URL implements java.io.Serializable { } + // BEGIN Android-added: Custom built-in URLStreamHandlers for http, https. + /** + * Returns an instance of the built-in handler for the given protocol, or null if none exists. + */ + private static URLStreamHandler createBuiltinHandler(String protocol) + throws ClassNotFoundException, InstantiationException, IllegalAccessException { + URLStreamHandler handler = null; + if (protocol.equals("file")) { + handler = new sun.net.www.protocol.file.Handler(); + } else if (protocol.equals("ftp")) { + handler = new sun.net.www.protocol.ftp.Handler(); + } else if (protocol.equals("jar")) { + handler = new sun.net.www.protocol.jar.Handler(); + } else if (protocol.equals("http")) { + handler = (URLStreamHandler)Class. + forName("com.android.okhttp.HttpHandler").newInstance(); + } else if (protocol.equals("https")) { + handler = (URLStreamHandler)Class. + forName("com.android.okhttp.HttpsHandler").newInstance(); + } + return handler; + } + + /** Names of implementation classes returned by {@link #createBuiltinHandler(String)}. */ + private static Set<String> createBuiltinHandlerClassNames() { + Set<String> result = new HashSet<>(); + // Refer to class names rather than classes to avoid needlessly triggering <clinit>. + result.add("sun.net.www.protocol.file.Handler"); + result.add("sun.net.www.protocol.ftp.Handler"); + result.add("sun.net.www.protocol.jar.Handler"); + result.add("com.android.okhttp.HttpHandler"); + result.add("com.android.okhttp.HttpsHandler"); + return Collections.unmodifiableSet(result); + } + // END Android-added: Custom built-in URLStreamHandlers for http, https. + + /** + * @serialField protocol String + * + * @serialField host String + * + * @serialField port int + * + * @serialField authority String + * + * @serialField file String + * + * @serialField ref String + * + * @serialField hashCode int + * + */ + private static final ObjectStreamField[] serialPersistentFields = { + new ObjectStreamField("protocol", String.class), + new ObjectStreamField("host", String.class), + new ObjectStreamField("port", int.class), + new ObjectStreamField("authority", String.class), + new ObjectStreamField("file", String.class), + new ObjectStreamField("ref", String.class), + // Android-changed: App compat: hashCode should not be serialized. + // new ObjectStreamField("hashCode", int.class), }; + }; + /** * WriteObject is called to save the state of the URL to an * ObjectOutputStream. The handler is not saved since it is @@ -1295,16 +1355,69 @@ public final class URL implements java.io.Serializable { * stream handler. */ private synchronized void readObject(java.io.ObjectInputStream s) - throws IOException, ClassNotFoundException - { - s.defaultReadObject(); // read the fields - if ((handler = getURLStreamHandler(protocol)) == null) { + throws IOException, ClassNotFoundException { + GetField gf = s.readFields(); + String protocol = (String)gf.get("protocol", null); + if (getURLStreamHandler(protocol) == null) { throw new IOException("unknown protocol: " + protocol); } + String host = (String)gf.get("host", null); + int port = gf.get("port", -1); + String authority = (String)gf.get("authority", null); + String file = (String)gf.get("file", null); + String ref = (String)gf.get("ref", null); + // Android-changed: App compat: hashCode should not be serialized. + // int hashCode = gf.get("hashCode", -1); + final int hashCode = -1; + if (authority == null + && ((host != null && host.length() > 0) || port != -1)) { + if (host == null) + host = ""; + authority = (port == -1) ? host : host + ":" + port; + } + tempState = new UrlDeserializedState(protocol, host, port, authority, + file, ref, hashCode); + } + + /** + * Replaces the de-serialized object with an URL object. + * + * @return a newly created object from the deserialzed state. + * + * @throws ObjectStreamException if a new object replacing this + * object could not be created + */ + + private Object readResolve() throws ObjectStreamException { + + URLStreamHandler handler = null; + // already been checked in readObject + handler = getURLStreamHandler(tempState.getProtocol()); + + URL replacementURL = null; + if (isBuiltinStreamHandler(handler.getClass().getName())) { + replacementURL = fabricateNewURL(); + } else { + replacementURL = setDeserializedFields(handler); + } + return replacementURL; + } + + private URL setDeserializedFields(URLStreamHandler handler) { + URL replacementURL; + String userInfo = null; + String protocol = tempState.getProtocol(); + String host = tempState.getHost(); + int port = tempState.getPort(); + String authority = tempState.getAuthority(); + String file = tempState.getFile(); + String ref = tempState.getRef(); + int hashCode = tempState.getHashCode(); + // Construct authority part - if (authority == null && - ((host != null && host.length() > 0) || port != -1)) { + if (authority == null + && ((host != null && host.length() > 0) || port != -1)) { if (host == null) host = ""; authority = (port == -1) ? host : host + ":" + port; @@ -1323,8 +1436,8 @@ public final class URL implements java.io.Serializable { } // Construct path and query part - path = null; - query = null; + String path = null; + String query = null; if (file != null) { // Fix: only do this if hierarchical? int q = file.lastIndexOf('?'); @@ -1334,7 +1447,66 @@ public final class URL implements java.io.Serializable { } else path = file; } - hashCode = -1; + + // Set the object fields. + this.protocol = protocol; + this.host = host; + this.port = port; + this.file = file; + this.authority = authority; + this.ref = ref; + this.hashCode = hashCode; + this.handler = handler; + this.query = query; + this.path = path; + this.userInfo = userInfo; + replacementURL = this; + return replacementURL; + } + + private URL fabricateNewURL() + throws InvalidObjectException { + // create URL string from deserialized object + URL replacementURL = null; + String urlString = tempState.reconstituteUrlString(); + + try { + replacementURL = new URL(urlString); + } catch (MalformedURLException mEx) { + resetState(); + InvalidObjectException invoEx = new InvalidObjectException( + "Malformed URL: " + urlString); + invoEx.initCause(mEx); + throw invoEx; + } + replacementURL.setSerializedHashCode(tempState.getHashCode()); + resetState(); + return replacementURL; + } + + private boolean isBuiltinStreamHandler(String handlerClassName) { + // Android-changed: Some built-in handlers (eg. HttpHandler) are not in sun.net.www.protocol. + // return (handlerClassName.startsWith(BUILTIN_HANDLERS_PREFIX)); + return BUILTIN_HANDLER_CLASS_NAMES.contains(handlerClassName); + } + + private void resetState() { + this.protocol = null; + this.host = null; + this.port = -1; + this.file = null; + this.authority = null; + this.ref = null; + this.hashCode = -1; + this.handler = null; + this.query = null; + this.path = null; + this.userInfo = null; + this.tempState = null; + } + + private void setSerializedHashCode(int hc) { + this.hashCode = hc; } } @@ -1374,3 +1546,82 @@ class Parts { return ref; } } + +final class UrlDeserializedState { + private final String protocol; + private final String host; + private final int port; + private final String authority; + private final String file; + private final String ref; + private final int hashCode; + + public UrlDeserializedState(String protocol, + String host, int port, + String authority, String file, + String ref, int hashCode) { + this.protocol = protocol; + this.host = host; + this.port = port; + this.authority = authority; + this.file = file; + this.ref = ref; + this.hashCode = hashCode; + } + + String getProtocol() { + return protocol; + } + + String getHost() { + return host; + } + + String getAuthority () { + return authority; + } + + int getPort() { + return port; + } + + String getFile () { + return file; + } + + String getRef () { + return ref; + } + + int getHashCode () { + return hashCode; + } + + String reconstituteUrlString() { + + // pre-compute length of StringBuilder + int len = protocol.length() + 1; + if (authority != null && authority.length() > 0) + len += 2 + authority.length(); + if (file != null) { + len += file.length(); + } + if (ref != null) + len += 1 + ref.length(); + StringBuilder result = new StringBuilder(len); + result.append(protocol); + result.append(":"); + if (authority != null && authority.length() > 0) { + result.append("//"); + result.append(authority); + } + if (file != null) { + result.append(file); + } + if (ref != null) { + result.append("#"); + result.append(ref); + } + return result.toString(); + } +} |