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