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