1 /*
2  * Copyright 2018 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 androidx.webkit;
18 
19 import static androidx.webkit.ProxyConfig.MATCH_ALL_SCHEMES;
20 import static androidx.webkit.ProxyConfig.MATCH_HTTP;
21 
22 import static org.junit.Assert.assertEquals;
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.fail;
25 
26 import androidx.concurrent.futures.ResolvableFuture;
27 import androidx.test.ext.junit.runners.AndroidJUnit4;
28 import androidx.test.filters.MediumTest;
29 import androidx.webkit.internal.ProxyControllerImpl;
30 import androidx.webkit.test.common.WebViewOnUiThread;
31 import androidx.webkit.test.common.WebkitUtils;
32 
33 import org.junit.After;
34 import org.junit.Assert;
35 import org.junit.Before;
36 import org.junit.Test;
37 import org.junit.runner.RunWith;
38 
39 import java.io.IOException;
40 import java.util.concurrent.Executor;
41 import java.util.concurrent.TimeUnit;
42 
43 import okhttp3.mockwebserver.MockWebServer;
44 
45 @MediumTest
46 @RunWith(AndroidJUnit4.class)
47 public class ProxyControllerTest {
48     private WebViewOnUiThread mWebViewOnUiThread;
49     private MockWebServer mContentServer;
50     private MockWebServer mProxyServer;
51 
52     @Before
setUp()53     public void setUp() throws IOException {
54         WebkitUtils.checkFeature(WebViewFeature.PROXY_OVERRIDE);
55 
56         mWebViewOnUiThread = new WebViewOnUiThread();
57         mContentServer = new MockWebServer();
58         mProxyServer = new MockWebServer();
59         mContentServer.start();
60         mProxyServer.start();
61     }
62 
63     @After
tearDown()64     public void tearDown() throws Exception {
65         if (!WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) return;
66 
67         clearProxyOverrideSync();
68         if (mWebViewOnUiThread != null) {
69             mWebViewOnUiThread.cleanUp();
70         }
71         if (mContentServer != null) {
72             mContentServer.shutdown();
73         }
74         if (mProxyServer != null) {
75             mProxyServer.shutdown();
76         }
77     }
78 
79     /**
80      * This test should have an equivalent in CTS when this is implemented in the framework.
81      *
82      * Tests that ClearProxyOverride callback is called even if there is no settings to clear.
83      */
84     @Test
testCallbacks()85     public void testCallbacks() throws Exception {
86         // Test setProxyOverride's callback
87         setProxyOverrideSync(new ProxyConfig.Builder().build());
88         // Test clearProxyOverride's callback with a proxy override setting
89         clearProxyOverrideSync();
90         // Test clearProxyOverride's callback without a proxy override setting
91         clearProxyOverrideSync();
92         // If we got to this point it means all callbacks were called as expected
93     }
94 
95     /**
96      * This test should have an equivalent in CTS when this is implemented in the framework.
97      */
98     @Test
testProxyOverride()99     public void testProxyOverride() throws Exception {
100         final String contentUrl = mContentServer.url("/").toString();
101         final String proxyUrl = mProxyServer.getHostName() + ":" + mProxyServer.getPort();
102 
103         // Clear proxy override and load content url
104         clearProxyOverrideSync();
105         mWebViewOnUiThread.loadUrl(contentUrl);
106         assertNotNull(mContentServer.takeRequest(WebkitUtils.TEST_TIMEOUT_MS,
107                 TimeUnit.MILLISECONDS));
108         int contentServerRequestCount = mContentServer.getRequestCount();
109 
110         // Set proxy override and load content url
111         // Localhost should use proxy with loopback rule
112         setProxyOverrideSync(new ProxyConfig.Builder()
113                 .addProxyRule(proxyUrl)
114                 .removeImplicitRules()
115                 .build());
116         mWebViewOnUiThread.loadUrl(contentUrl);
117         assertNotNull(mProxyServer.takeRequest(WebkitUtils.TEST_TIMEOUT_MS,
118                 TimeUnit.MILLISECONDS));
119         int proxyServerRequestCount = mProxyServer.getRequestCount();
120         assertEquals(contentServerRequestCount, mContentServer.getRequestCount());
121 
122         // Clear proxy override and load content url
123         clearProxyOverrideSync();
124         mWebViewOnUiThread.loadUrl(contentUrl);
125         assertNotNull(mContentServer.takeRequest(WebkitUtils.TEST_TIMEOUT_MS,
126                 TimeUnit.MILLISECONDS));
127         assertEquals(proxyServerRequestCount, mProxyServer.getRequestCount());
128     }
129 
130     /**
131      * This test should have an equivalent in CTS when this is implemented in the framework.
132      */
133     @Test
testProxyOverrideLocalhost()134     public void testProxyOverrideLocalhost() throws Exception {
135         final String contentUrl = mContentServer.url("/").toString();
136         int proxyServerRequestCount = mProxyServer.getRequestCount();
137 
138         // Set proxy override and load content url
139         // Localhost should not use proxy settings
140         setProxyOverrideSync(new ProxyConfig.Builder()
141                 .addProxyRule(mProxyServer.getHostName() + ":" + mProxyServer.getPort())
142                 .build());
143         mWebViewOnUiThread.loadUrl(contentUrl);
144         assertNotNull(mContentServer.takeRequest(WebkitUtils.TEST_TIMEOUT_MS,
145                 TimeUnit.MILLISECONDS));
146         assertEquals(proxyServerRequestCount, mProxyServer.getRequestCount());
147     }
148 
149     /**
150      * This test should have an equivalent in CTS when this is implemented in the framework.
151      */
152     @Test
testReverseBypass()153     public void testReverseBypass() throws Exception {
154         WebkitUtils.checkFeature(WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS);
155 
156         final String contentUrl = "http://www.example.com";
157         final String bypassUrl = "www.example.com";
158         int proxyServerRequestCount = mProxyServer.getRequestCount();
159 
160         // Set proxy override with reverse bypass and load content url
161         // The content url (in the bypass list) should use proxy settings.
162         setProxyOverrideSync(new ProxyConfig.Builder()
163                 .addProxyRule(mProxyServer.getHostName() + ":" + mProxyServer.getPort())
164                 .addBypassRule(bypassUrl)
165                 .setReverseBypassEnabled(true)
166                 .build());
167         mWebViewOnUiThread.loadUrl(contentUrl);
168 
169         proxyServerRequestCount++;
170         assertNotNull(mProxyServer.takeRequest(WebkitUtils.TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
171         assertEquals(proxyServerRequestCount, mProxyServer.getRequestCount());
172     }
173 
174     /**
175      * This test should have an equivalent in CTS when this is implemented in the framework.
176      *
177      * Enumerates valid patterns to check they are supported.
178      */
179     @Test
testValidInput()180     public void testValidInput() throws Exception {
181         ProxyConfig validRules = new ProxyConfig.Builder()
182                 .addProxyRule("direct://")
183                 .addProxyRule("www.example.com")
184                 .addProxyRule("http://www.example.com")
185                 .addProxyRule("https://www.example.com")
186                 .addProxyRule("www.example.com:123")
187                 .addProxyRule("http://www.example.com:123")
188                 .addProxyRule("10.0.0.1")
189                 .addProxyRule("10.0.0.1:123")
190                 .addProxyRule("http://10.0.0.1")
191                 .addProxyRule("https://10.0.0.1")
192                 .addProxyRule("http://10.0.0.1:123")
193                 .addProxyRule("[FE80:CD00:0000:0CDE:1257:0000:211E:729C]")
194                 .addProxyRule("[FE80:CD00:0:CDE:1257:0:211E:729C]")
195                 .addBypassRule("www.rule.com")
196                 .addBypassRule("*.rule.com")
197                 .addBypassRule("*rule.com")
198                 .addBypassRule("www.*.com")
199                 .addBypassRule("www.rule*")
200                 .build();
201         setProxyOverrideSync(validRules);
202         // If we got to this point it means our input was accepted as expected
203     }
204 
205     /**
206      * This test should have an equivalent in CTS when this is implemented in the framework.
207      */
208     @Test
testInvalidProxyUrls()209     public void testInvalidProxyUrls() throws Exception {
210         String[] invalidProxyUrls = {
211                 null,
212                 "", // empty
213                 "   ",  // spaces only
214                 "dddf:", // bad port
215                 "dddd:d", // bad port
216                 "http://", // no valid host/port
217                 "http:/", // ambiguous, will fail due to bad port
218                 "http:", // ambiguous, will fail due to bad port
219                 "direct://xyz", // direct shouldn't have host/port
220         };
221 
222         for (String proxyUrl : invalidProxyUrls) {
223             try {
224                 setProxyOverrideSync(new ProxyConfig.Builder()
225                         .addProxyRule(proxyUrl).build());
226                 fail("No exception for invalid proxy url: " + proxyUrl);
227             } catch (IllegalArgumentException e) {
228                 // Expected
229             }
230         }
231     }
232 
233     /**
234      * This test should have an equivalent in CTS when this is implemented in the framework.
235      */
236     @Test
testInvalidBypassRules()237     public void testInvalidBypassRules() throws Exception {
238         String[] invalidBypassRules = {
239                 null,
240                 "", // empty
241                 "http://", // no valid host/port
242                 "20:example.com", // bad port
243                 "example.com:-20" // bad port
244         };
245 
246         for (String bypassRule : invalidBypassRules) {
247             try {
248                 setProxyOverrideSync(new ProxyConfig.Builder()
249                         .addBypassRule(bypassRule).build());
250                 fail("No exception for invalid bypass rule: " + bypassRule);
251             } catch (IllegalArgumentException e) {
252                 // Expected
253             }
254         }
255     }
256 
257     @Test
testProxyRulesToArray()258     public void testProxyRulesToArray() throws Exception {
259         String[][] actual;
260         String[][] expected;
261 
262         actual = ProxyControllerImpl.proxyRulesToStringArray(
263                 new ProxyConfig.Builder().build().getProxyRules());
264         expected = new String[][]{};
265         Assert.assertArrayEquals(expected, actual);
266 
267         actual = ProxyControllerImpl.proxyRulesToStringArray(
268                 new ProxyConfig.Builder().addProxyRule("proxy1.com").build().getProxyRules());
269         expected = new String[][]{{MATCH_ALL_SCHEMES, "proxy1.com"}};
270         Assert.assertArrayEquals(expected, actual);
271 
272         actual = ProxyControllerImpl.proxyRulesToStringArray(
273                 new ProxyConfig.Builder().addProxyRule("proxy1.com").addProxyRule(
274                         "proxy2.com").build().getProxyRules());
275         expected = new String[][]{{MATCH_ALL_SCHEMES, "proxy1.com"},
276                 {MATCH_ALL_SCHEMES, "proxy2.com"}};
277         Assert.assertArrayEquals(expected, actual);
278 
279         actual = ProxyControllerImpl.proxyRulesToStringArray(
280                 new ProxyConfig.Builder().addProxyRule("proxy1.com").addProxyRule("proxy2.com",
281                         MATCH_HTTP).build().getProxyRules());
282         expected = new String[][]{{MATCH_ALL_SCHEMES, "proxy1.com"}, {MATCH_HTTP, "proxy2.com"}};
283         Assert.assertArrayEquals(expected, actual);
284     }
285 
setProxyOverrideSync(final ProxyConfig proxyRules)286     private void setProxyOverrideSync(final ProxyConfig proxyRules) {
287         final ResolvableFuture<Void> future = ResolvableFuture.create();
288         ProxyController.getInstance().setProxyOverride(proxyRules, new SynchronousExecutor(),
289                 () -> future.set(null));
290         // This future is used to ensure that setProxyOverride's callback was called
291         WebkitUtils.waitForFuture(future);
292     }
293 
clearProxyOverrideSync()294     private void clearProxyOverrideSync() {
295         final ResolvableFuture<Void> future = ResolvableFuture.create();
296         ProxyController.getInstance().clearProxyOverride(
297                 new SynchronousExecutor(), () -> future.set(null));
298         // This future is used to ensure that clearProxyOverride's callback was called
299         WebkitUtils.waitForFuture(future);
300     }
301 
302     static class SynchronousExecutor implements Executor {
303         @Override
execute(Runnable r)304         public void execute(Runnable r) {
305             r.run();
306         }
307     }
308 }
309