1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package libcore.libcore.net; 18 19 import junit.framework.TestCase; 20 import libcore.io.IoUtils; 21 import libcore.net.NetworkSecurityPolicy; 22 import java.io.Closeable; 23 import java.io.IOException; 24 import java.net.JarURLConnection; 25 import java.net.ServerSocket; 26 import java.net.Socket; 27 import java.net.URL; 28 import java.util.Arrays; 29 import java.util.HashMap; 30 import java.util.Map; 31 import java.util.concurrent.Callable; 32 import java.util.concurrent.ExecutorService; 33 import java.util.concurrent.Executors; 34 import java.util.concurrent.Future; 35 import java.util.concurrent.TimeUnit; 36 import java.util.concurrent.TimeoutException; 37 import java.util.logging.ErrorManager; 38 import java.util.logging.Level; 39 import java.util.logging.LogRecord; 40 import java.util.logging.SocketHandler; 41 42 public class NetworkSecurityPolicyTest extends TestCase { 43 44 private NetworkSecurityPolicy mOriginalPolicy; 45 46 @Override setUp()47 protected void setUp() throws Exception { 48 super.setUp(); 49 mOriginalPolicy = NetworkSecurityPolicy.getInstance(); 50 } 51 52 @Override tearDown()53 protected void tearDown() throws Exception { 54 try { 55 NetworkSecurityPolicy.setInstance(mOriginalPolicy); 56 } finally { 57 super.tearDown(); 58 } 59 } 60 testCleartextTrafficPolicySetterAndGetter()61 public void testCleartextTrafficPolicySetterAndGetter() { 62 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(false)); 63 assertEquals(false, NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted()); 64 65 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(true)); 66 assertEquals(true, NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted()); 67 68 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(false)); 69 assertEquals(false, NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted()); 70 71 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(true)); 72 assertEquals(true, NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted()); 73 } 74 testHostnameAwareCleartextTrafficPolicySetterAndGetter()75 public void testHostnameAwareCleartextTrafficPolicySetterAndGetter() { 76 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(false)); 77 assertEquals(false, 78 NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted("localhost")); 79 80 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(true)); 81 assertEquals(true, 82 NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted("localhost")); 83 84 TestNetworkSecurityPolicy policy = new TestNetworkSecurityPolicy(false); 85 policy.addHostMapping("localhost", true); 86 policy.addHostMapping("example.com", false); 87 NetworkSecurityPolicy.setInstance(policy); 88 assertEquals(false, NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted()); 89 assertEquals(true, 90 NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted("localhost")); 91 assertEquals(false, 92 NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted("example.com")); 93 94 } 95 testCleartextTrafficPolicyWithHttpURLConnection()96 public void testCleartextTrafficPolicyWithHttpURLConnection() throws Exception { 97 // Assert that client transmits some data when cleartext traffic is permitted. 98 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(true)); 99 try (CapturingServerSocket server = new CapturingServerSocket()) { 100 URL url = new URL("http://localhost:" + server.getPort() + "/test.txt"); 101 try { 102 url.openConnection().getContent(); 103 fail(); 104 } catch (IOException expected) { 105 } 106 server.assertDataTransmittedByClient(); 107 } 108 109 // Assert that client does not transmit any data when cleartext traffic is not permitted and 110 // that URLConnection.openConnection or getContent fail with an IOException. 111 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(false)); 112 try (CapturingServerSocket server = new CapturingServerSocket()) { 113 URL url = new URL("http://localhost:" + server.getPort() + "/test.txt"); 114 try { 115 url.openConnection().getContent(); 116 fail(); 117 } catch (IOException expected) { 118 } 119 server.assertNoDataTransmittedByClient(); 120 } 121 } 122 testCleartextTrafficPolicyWithFtpURLConnection()123 public void testCleartextTrafficPolicyWithFtpURLConnection() throws Exception { 124 // Assert that client transmits some data when cleartext traffic is permitted. 125 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(true)); 126 byte[] serverReplyOnConnect = "220\r\n".getBytes("US-ASCII"); 127 try (CapturingServerSocket server = new CapturingServerSocket(serverReplyOnConnect)) { 128 URL url = new URL("ftp://localhost:" + server.getPort() + "/test.txt"); 129 try { 130 url.openConnection().getContent(); 131 fail(); 132 } catch (IOException expected) { 133 } 134 server.assertDataTransmittedByClient(); 135 } 136 137 // Assert that client does not transmit any data when cleartext traffic is not permitted and 138 // that URLConnection.openConnection or getContent fail with an IOException. 139 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(false)); 140 try (CapturingServerSocket server = new CapturingServerSocket(serverReplyOnConnect)) { 141 URL url = new URL("ftp://localhost:" + server.getPort() + "/test.txt"); 142 try { 143 url.openConnection().getContent(); 144 fail(); 145 } catch (IOException expected) { 146 } 147 server.assertNoDataTransmittedByClient(); 148 } 149 } 150 testCleartextTrafficPolicyWithJarHttpURLConnection()151 public void testCleartextTrafficPolicyWithJarHttpURLConnection() throws Exception { 152 // Assert that client transmits some data when cleartext traffic is permitted. 153 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(true)); 154 try (CapturingServerSocket server = new CapturingServerSocket()) { 155 URL url = new URL("jar:http://localhost:" + server.getPort() + "/test.jar!/"); 156 try { 157 ((JarURLConnection) url.openConnection()).getManifest(); 158 fail(); 159 } catch (IOException expected) { 160 } 161 server.assertDataTransmittedByClient(); 162 } 163 164 // Assert that client does not transmit any data when cleartext traffic is not permitted and 165 // that JarURLConnection.openConnection or getManifest fail with an IOException. 166 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(false)); 167 try (CapturingServerSocket server = new CapturingServerSocket()) { 168 URL url = new URL("jar:http://localhost:" + server.getPort() + "/test.jar!/"); 169 try { 170 ((JarURLConnection) url.openConnection()).getManifest(); 171 fail(); 172 } catch (IOException expected) { 173 } 174 server.assertNoDataTransmittedByClient(); 175 } 176 } 177 testCleartextTrafficPolicyWithJarFtpURLConnection()178 public void testCleartextTrafficPolicyWithJarFtpURLConnection() throws Exception { 179 // Assert that client transmits some data when cleartext traffic is permitted. 180 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(true)); 181 byte[] serverReplyOnConnect = "220\r\n".getBytes("US-ASCII"); 182 try (CapturingServerSocket server = new CapturingServerSocket(serverReplyOnConnect)) { 183 URL url = new URL("jar:ftp://localhost:" + server.getPort() + "/test.jar!/"); 184 try { 185 ((JarURLConnection) url.openConnection()).getManifest(); 186 fail(); 187 } catch (IOException expected) { 188 } 189 server.assertDataTransmittedByClient(); 190 } 191 192 // Assert that client does not transmit any data when cleartext traffic is not permitted and 193 // that JarURLConnection.openConnection or getManifest fail with an IOException. 194 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(false)); 195 try (CapturingServerSocket server = new CapturingServerSocket(serverReplyOnConnect)) { 196 URL url = new URL("jar:ftp://localhost:" + server.getPort() + "/test.jar!/"); 197 try { 198 ((JarURLConnection) url.openConnection()).getManifest(); 199 fail(); 200 } catch (IOException expected) { 201 } 202 server.assertNoDataTransmittedByClient(); 203 } 204 } 205 testCleartextTrafficPolicyWithLoggingSocketHandler()206 public void testCleartextTrafficPolicyWithLoggingSocketHandler() throws Exception { 207 // Assert that client transmits some data when cleartext traffic is permitted. 208 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(true)); 209 try (CapturingServerSocket server = new CapturingServerSocket()) { 210 SocketHandler logger = new SocketHandler("localhost", server.getPort()); 211 MockErrorManager mockErrorManager = new MockErrorManager(); 212 logger.setErrorManager(mockErrorManager); 213 logger.setLevel(Level.ALL); 214 LogRecord record = new LogRecord(Level.INFO, "A log record"); 215 assertTrue(logger.isLoggable(record)); 216 logger.publish(record); 217 assertNull(mockErrorManager.getMostRecentException()); 218 server.assertDataTransmittedByClient(); 219 logger.close(); 220 } 221 222 // Assert that client does not transmit any data when cleartext traffic is not permitted. 223 NetworkSecurityPolicy.setInstance(new TestNetworkSecurityPolicy(false)); 224 try (CapturingServerSocket server = new CapturingServerSocket()) { 225 try { 226 new SocketHandler("localhost", server.getPort()); 227 fail(); 228 } catch (IOException expected) { 229 } 230 server.assertNoDataTransmittedByClient(); 231 } 232 } 233 234 /** 235 * Server socket which listens on a local port and captures the first chunk of data transmitted 236 * by the client. 237 */ 238 private static class CapturingServerSocket implements Closeable { 239 private final ServerSocket mSocket; 240 private final int mPort; 241 private final ExecutorService executor; 242 private final Future<byte[]> mFirstChunkReceivedFuture; 243 244 /** 245 * Constructs a new socket listening on a local port. 246 */ CapturingServerSocket()247 public CapturingServerSocket() throws IOException { 248 this(null); 249 } 250 251 /** 252 * Constructs a new socket listening on a local port, which sends the provided reply as 253 * soon as a client connects to it. 254 */ CapturingServerSocket(final byte[] replyOnConnect)255 public CapturingServerSocket(final byte[] replyOnConnect) throws IOException { 256 mSocket = new ServerSocket(0); 257 mPort = mSocket.getLocalPort(); 258 Callable<byte[]> callable = () -> { 259 try (Socket client = mSocket.accept()) { 260 // Reply (if requested) 261 if (replyOnConnect != null) { 262 client.getOutputStream().write(replyOnConnect); 263 client.getOutputStream().flush(); 264 } 265 266 // Read request 267 byte[] buf = new byte[64 * 1024]; 268 int chunkSize = client.getInputStream().read(buf); 269 if (chunkSize == -1) { 270 // Connection closed without any data received 271 return new byte[0]; 272 } 273 // Received some data 274 return Arrays.copyOf(buf, chunkSize); 275 } finally { 276 IoUtils.closeQuietly(mSocket); 277 } 278 }; 279 executor = Executors.newSingleThreadExecutor(); 280 mFirstChunkReceivedFuture = executor.submit(callable); 281 } 282 getPort()283 public int getPort() { 284 return mPort; 285 } 286 getFirstReceivedChunkFuture()287 public Future<byte[]> getFirstReceivedChunkFuture() { 288 return mFirstChunkReceivedFuture; 289 } 290 291 @Override close()292 public void close() { 293 IoUtils.closeQuietly(mSocket); 294 executor.shutdown(); 295 } 296 assertDataTransmittedByClient()297 private void assertDataTransmittedByClient() 298 throws Exception { 299 byte[] firstChunkFromClient = getFirstReceivedChunkFuture().get(4, TimeUnit.SECONDS); 300 if ((firstChunkFromClient == null) || (firstChunkFromClient.length == 0)) { 301 fail("Client did not transmit any data to server"); 302 } 303 } 304 assertNoDataTransmittedByClient()305 private void assertNoDataTransmittedByClient() 306 throws Exception { 307 byte[] firstChunkFromClient; 308 try { 309 firstChunkFromClient = getFirstReceivedChunkFuture().get(4, TimeUnit.SECONDS); 310 } catch (TimeoutException expected) { 311 return; 312 } 313 if ((firstChunkFromClient != null) && (firstChunkFromClient.length > 0)) { 314 fail("Client transmitted " + firstChunkFromClient.length+ " bytes: " 315 + new String(firstChunkFromClient, "US-ASCII")); 316 } 317 } 318 } 319 320 private static class MockErrorManager extends ErrorManager { 321 private Exception mMostRecentException; 322 getMostRecentException()323 public Exception getMostRecentException() { 324 synchronized (this) { 325 return mMostRecentException; 326 } 327 } 328 329 @Override error(String message, Exception exception, int errorCode)330 public void error(String message, Exception exception, int errorCode) { 331 synchronized (this) { 332 mMostRecentException = exception; 333 } 334 } 335 } 336 337 private static class TestNetworkSecurityPolicy extends NetworkSecurityPolicy { 338 private final boolean mCleartextTrafficPermitted; 339 private final Map<String, Boolean> mHostMap = new HashMap<String, Boolean>(); 340 TestNetworkSecurityPolicy(boolean cleartextTrafficPermitted)341 public TestNetworkSecurityPolicy(boolean cleartextTrafficPermitted) { 342 mCleartextTrafficPermitted = cleartextTrafficPermitted; 343 } 344 addHostMapping(String hostname, boolean isCleartextTrafficPermitted)345 public void addHostMapping(String hostname, boolean isCleartextTrafficPermitted) { 346 mHostMap.put(hostname, isCleartextTrafficPermitted); 347 } 348 349 @Override isCleartextTrafficPermitted()350 public boolean isCleartextTrafficPermitted() { 351 return mCleartextTrafficPermitted; 352 } 353 354 @Override isCleartextTrafficPermitted(String hostname)355 public boolean isCleartextTrafficPermitted(String hostname) { 356 if (mHostMap.containsKey(hostname)) { 357 return mHostMap.get(hostname); 358 } 359 360 return isCleartextTrafficPermitted(); 361 } 362 363 @Override isCertificateTransparencyVerificationRequired(String hostname)364 public boolean isCertificateTransparencyVerificationRequired(String hostname) { 365 return false; 366 } 367 } 368 } 369