Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2017 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 com.android.server.connectivity; |
| 18 | |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 19 | import static junit.framework.Assert.assertFalse; |
| 20 | |
| 21 | import static org.junit.Assert.assertTrue; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 22 | import static org.junit.Assert.fail; |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 23 | import static org.mockito.ArgumentMatchers.anyString; |
| 24 | import static org.mockito.ArgumentMatchers.eq; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 25 | import static org.mockito.Mockito.any; |
| 26 | import static org.mockito.Mockito.anyInt; |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 27 | import static org.mockito.Mockito.never; |
| 28 | import static org.mockito.Mockito.times; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 29 | import static org.mockito.Mockito.verify; |
| 30 | import static org.mockito.Mockito.when; |
| 31 | |
| 32 | import android.content.Context; |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 33 | import android.net.ConnectivityManager; |
| 34 | import android.net.LinkProperties; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 35 | import android.net.Network; |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 36 | import android.net.NetworkCapabilities; |
| 37 | import android.net.NetworkInfo; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 38 | import android.net.NetworkRequest; |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 39 | import android.net.captiveportal.CaptivePortalProbeResult; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 40 | import android.net.metrics.IpConnectivityLog; |
| 41 | import android.net.wifi.WifiManager; |
| 42 | import android.os.Handler; |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 43 | import android.provider.Settings; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 44 | import android.support.test.filters.SmallTest; |
| 45 | import android.support.test.runner.AndroidJUnit4; |
| 46 | import android.telephony.TelephonyManager; |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 47 | import android.util.ArrayMap; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 48 | |
| 49 | import org.junit.Before; |
| 50 | import org.junit.Test; |
| 51 | import org.junit.runner.RunWith; |
| 52 | import org.mockito.Mock; |
| 53 | import org.mockito.MockitoAnnotations; |
| 54 | |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 55 | import java.io.IOException; |
| 56 | import java.net.HttpURLConnection; |
| 57 | import java.net.InetAddress; |
| 58 | import java.net.URL; |
| 59 | import java.util.Random; |
| 60 | |
| 61 | import javax.net.ssl.SSLHandshakeException; |
| 62 | |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 63 | |
| 64 | @RunWith(AndroidJUnit4.class) |
| 65 | @SmallTest |
| 66 | public class NetworkMonitorTest { |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 67 | private static final String LOCATION_HEADER = "location"; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 68 | |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 69 | private @Mock Context mContext; |
| 70 | private @Mock Handler mHandler; |
| 71 | private @Mock IpConnectivityLog mLogger; |
| 72 | private @Mock NetworkAgentInfo mAgent; |
| 73 | private @Mock NetworkInfo mNetworkInfo; |
| 74 | private @Mock NetworkRequest mRequest; |
| 75 | private @Mock TelephonyManager mTelephony; |
| 76 | private @Mock WifiManager mWifi; |
| 77 | private @Mock Network mNetwork; |
| 78 | private @Mock HttpURLConnection mHttpConnection; |
| 79 | private @Mock HttpURLConnection mHttpsConnection; |
| 80 | private @Mock HttpURLConnection mFallbackConnection; |
| 81 | private @Mock HttpURLConnection mOtherFallbackConnection; |
| 82 | private @Mock Random mRandom; |
| 83 | private @Mock NetworkMonitor.Dependencies mDependencies; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 84 | |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 85 | private static final String TEST_HTTP_URL = "http://www.google.com/gen_204"; |
| 86 | private static final String TEST_HTTPS_URL = "https://www.google.com/gen_204"; |
| 87 | private static final String TEST_FALLBACK_URL = "http://fallback.google.com/gen_204"; |
| 88 | private static final String TEST_OTHER_FALLBACK_URL = "http://otherfallback.google.com/gen_204"; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 89 | |
| 90 | @Before |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 91 | public void setUp() throws IOException { |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 92 | MockitoAnnotations.initMocks(this); |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 93 | mAgent.linkProperties = new LinkProperties(); |
| 94 | mAgent.networkCapabilities = new NetworkCapabilities() |
| 95 | .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); |
| 96 | mAgent.networkInfo = mNetworkInfo; |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 97 | |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 98 | when(mAgent.network()).thenReturn(mNetwork); |
| 99 | when(mDependencies.getNetwork(any())).thenReturn(mNetwork); |
| 100 | when(mDependencies.getRandom()).thenReturn(mRandom); |
| 101 | when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_MODE), anyInt())) |
| 102 | .thenReturn(Settings.Global.CAPTIVE_PORTAL_MODE_PROMPT); |
| 103 | when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_USE_HTTPS), |
| 104 | anyInt())).thenReturn(1); |
| 105 | when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTP_URL), |
| 106 | anyString())).thenReturn(TEST_HTTP_URL); |
| 107 | when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTPS_URL), |
| 108 | anyString())).thenReturn(TEST_HTTPS_URL); |
Erik Kline | f4fa982 | 2018-04-27 22:48:33 +0900 | [diff] [blame] | 109 | when(mNetwork.getPrivateDnsBypassingCopy()).thenReturn(mNetwork); |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 110 | |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 111 | when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephony); |
| 112 | when(mContext.getSystemService(Context.WIFI_SERVICE)).thenReturn(mWifi); |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 113 | |
| 114 | when(mNetworkInfo.getType()).thenReturn(ConnectivityManager.TYPE_WIFI); |
| 115 | setFallbackUrl(TEST_FALLBACK_URL); |
| 116 | setOtherFallbackUrls(TEST_OTHER_FALLBACK_URL); |
| 117 | setFallbackSpecs(null); // Test with no fallback spec by default |
| 118 | when(mRandom.nextInt()).thenReturn(0); |
| 119 | |
| 120 | when(mNetwork.openConnection(any())).then((invocation) -> { |
| 121 | URL url = invocation.getArgument(0); |
| 122 | switch(url.toString()) { |
| 123 | case TEST_HTTP_URL: |
| 124 | return mHttpConnection; |
| 125 | case TEST_HTTPS_URL: |
| 126 | return mHttpsConnection; |
| 127 | case TEST_FALLBACK_URL: |
| 128 | return mFallbackConnection; |
| 129 | case TEST_OTHER_FALLBACK_URL: |
| 130 | return mOtherFallbackConnection; |
| 131 | default: |
| 132 | fail("URL not mocked: " + url.toString()); |
| 133 | return null; |
| 134 | } |
| 135 | }); |
| 136 | when(mHttpConnection.getRequestProperties()).thenReturn(new ArrayMap<>()); |
| 137 | when(mHttpsConnection.getRequestProperties()).thenReturn(new ArrayMap<>()); |
| 138 | when(mNetwork.getAllByName(any())).thenReturn(new InetAddress[] { |
| 139 | InetAddress.parseNumericAddress("192.168.0.0") |
| 140 | }); |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 141 | } |
| 142 | |
| 143 | NetworkMonitor makeMonitor() { |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 144 | return new NetworkMonitor( |
| 145 | mContext, mHandler, mAgent, mRequest, mLogger, mDependencies); |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 146 | } |
| 147 | |
| 148 | @Test |
Remi NGUYEN VAN | d9a1cd7 | 2018-05-22 13:11:15 +0900 | [diff] [blame] | 149 | public void testIsCaptivePortal_HttpProbeIsPortal() throws IOException { |
| 150 | setSslException(mHttpsConnection); |
| 151 | setPortal302(mHttpConnection); |
| 152 | |
| 153 | assertPortal(makeMonitor().isCaptivePortal()); |
| 154 | } |
| 155 | |
| 156 | @Test |
| 157 | public void testIsCaptivePortal_HttpsProbeIsNotPortal() throws IOException { |
| 158 | setStatus(mHttpsConnection, 204); |
| 159 | setStatus(mHttpConnection, 500); |
| 160 | |
| 161 | assertNotPortal(makeMonitor().isCaptivePortal()); |
| 162 | } |
| 163 | |
| 164 | @Test |
| 165 | public void testIsCaptivePortal_HttpsProbeFailedHttpSuccessNotUsed() throws IOException { |
| 166 | setSslException(mHttpsConnection); |
| 167 | // Even if HTTP returns a 204, do not use the result unless HTTPS succeeded |
| 168 | setStatus(mHttpConnection, 204); |
| 169 | setStatus(mFallbackConnection, 500); |
| 170 | |
| 171 | assertFailed(makeMonitor().isCaptivePortal()); |
| 172 | } |
| 173 | |
| 174 | @Test |
| 175 | public void testIsCaptivePortal_FallbackProbeIsPortal() throws IOException { |
| 176 | setSslException(mHttpsConnection); |
| 177 | setStatus(mHttpConnection, 500); |
| 178 | setPortal302(mFallbackConnection); |
| 179 | |
| 180 | assertPortal(makeMonitor().isCaptivePortal()); |
| 181 | } |
| 182 | |
| 183 | @Test |
| 184 | public void testIsCaptivePortal_FallbackProbeIsNotPortal() throws IOException { |
| 185 | setSslException(mHttpsConnection); |
| 186 | setStatus(mHttpConnection, 500); |
| 187 | setStatus(mFallbackConnection, 204); |
| 188 | |
| 189 | // Fallback probe did not see portal, HTTPS failed -> inconclusive |
| 190 | assertFailed(makeMonitor().isCaptivePortal()); |
| 191 | } |
| 192 | |
| 193 | @Test |
| 194 | public void testIsCaptivePortal_OtherFallbackProbeIsPortal() throws IOException { |
| 195 | // Set all fallback probes but one to invalid URLs to verify they are being skipped |
| 196 | setFallbackUrl(TEST_FALLBACK_URL); |
| 197 | setOtherFallbackUrls(TEST_FALLBACK_URL + "," + TEST_OTHER_FALLBACK_URL); |
| 198 | |
| 199 | setSslException(mHttpsConnection); |
| 200 | setStatus(mHttpConnection, 500); |
| 201 | setStatus(mFallbackConnection, 500); |
| 202 | setPortal302(mOtherFallbackConnection); |
| 203 | |
| 204 | // TEST_OTHER_FALLBACK_URL is third |
| 205 | when(mRandom.nextInt()).thenReturn(2); |
| 206 | |
| 207 | final NetworkMonitor monitor = makeMonitor(); |
| 208 | |
| 209 | // First check always uses the first fallback URL: inconclusive |
| 210 | assertFailed(monitor.isCaptivePortal()); |
| 211 | verify(mFallbackConnection, times(1)).getResponseCode(); |
| 212 | verify(mOtherFallbackConnection, never()).getResponseCode(); |
| 213 | |
| 214 | // Second check uses the URL chosen by Random |
| 215 | assertPortal(monitor.isCaptivePortal()); |
| 216 | verify(mOtherFallbackConnection, times(1)).getResponseCode(); |
| 217 | } |
| 218 | |
| 219 | @Test |
| 220 | public void testIsCaptivePortal_AllProbesFailed() throws IOException { |
| 221 | setSslException(mHttpsConnection); |
| 222 | setStatus(mHttpConnection, 500); |
| 223 | setStatus(mFallbackConnection, 404); |
| 224 | |
| 225 | assertFailed(makeMonitor().isCaptivePortal()); |
| 226 | verify(mFallbackConnection, times(1)).getResponseCode(); |
| 227 | verify(mOtherFallbackConnection, never()).getResponseCode(); |
| 228 | } |
| 229 | |
| 230 | @Test |
| 231 | public void testIsCaptivePortal_InvalidUrlSkipped() throws IOException { |
| 232 | setFallbackUrl("invalid"); |
| 233 | setOtherFallbackUrls("otherinvalid," + TEST_OTHER_FALLBACK_URL + ",yetanotherinvalid"); |
| 234 | |
| 235 | setSslException(mHttpsConnection); |
| 236 | setStatus(mHttpConnection, 500); |
| 237 | setPortal302(mOtherFallbackConnection); |
| 238 | |
| 239 | assertPortal(makeMonitor().isCaptivePortal()); |
| 240 | verify(mOtherFallbackConnection, times(1)).getResponseCode(); |
| 241 | verify(mFallbackConnection, never()).getResponseCode(); |
| 242 | } |
| 243 | |
| 244 | private void setupFallbackSpec() throws IOException { |
| 245 | setFallbackSpecs("http://example.com@@/@@204@@/@@" |
| 246 | + "@@,@@" |
| 247 | + TEST_OTHER_FALLBACK_URL + "@@/@@30[12]@@/@@https://(www\\.)?google.com/?.*"); |
| 248 | |
| 249 | setSslException(mHttpsConnection); |
| 250 | setStatus(mHttpConnection, 500); |
| 251 | |
| 252 | // Use the 2nd fallback spec |
| 253 | when(mRandom.nextInt()).thenReturn(1); |
| 254 | } |
| 255 | |
| 256 | @Test |
| 257 | public void testIsCaptivePortal_FallbackSpecIsNotPortal() throws IOException { |
| 258 | setupFallbackSpec(); |
| 259 | set302(mOtherFallbackConnection, "https://www.google.com/test?q=3"); |
| 260 | |
| 261 | // HTTPS failed, fallback spec did not see a portal -> inconclusive |
| 262 | assertFailed(makeMonitor().isCaptivePortal()); |
| 263 | verify(mOtherFallbackConnection, times(1)).getResponseCode(); |
| 264 | verify(mFallbackConnection, never()).getResponseCode(); |
| 265 | } |
| 266 | |
| 267 | @Test |
| 268 | public void testIsCaptivePortal_FallbackSpecIsPortal() throws IOException { |
| 269 | setupFallbackSpec(); |
| 270 | set302(mOtherFallbackConnection, "http://login.portal.example.com"); |
| 271 | |
| 272 | assertPortal(makeMonitor().isCaptivePortal()); |
| 273 | } |
| 274 | |
| 275 | private void setFallbackUrl(String url) { |
| 276 | when(mDependencies.getSetting(any(), |
| 277 | eq(Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL), any())).thenReturn(url); |
| 278 | } |
| 279 | |
| 280 | private void setOtherFallbackUrls(String urls) { |
| 281 | when(mDependencies.getSetting(any(), |
| 282 | eq(Settings.Global.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS), any())).thenReturn(urls); |
| 283 | } |
| 284 | |
| 285 | private void setFallbackSpecs(String specs) { |
| 286 | when(mDependencies.getSetting(any(), |
| 287 | eq(Settings.Global.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS), any())).thenReturn(specs); |
| 288 | } |
| 289 | |
| 290 | private void assertPortal(CaptivePortalProbeResult result) { |
| 291 | assertTrue(result.isPortal()); |
| 292 | assertFalse(result.isFailed()); |
| 293 | assertFalse(result.isSuccessful()); |
| 294 | } |
| 295 | |
| 296 | private void assertNotPortal(CaptivePortalProbeResult result) { |
| 297 | assertFalse(result.isPortal()); |
| 298 | assertFalse(result.isFailed()); |
| 299 | assertTrue(result.isSuccessful()); |
| 300 | } |
| 301 | |
| 302 | private void assertFailed(CaptivePortalProbeResult result) { |
| 303 | assertFalse(result.isPortal()); |
| 304 | assertTrue(result.isFailed()); |
| 305 | assertFalse(result.isSuccessful()); |
| 306 | } |
| 307 | |
| 308 | private void setSslException(HttpURLConnection connection) throws IOException { |
| 309 | when(connection.getResponseCode()).thenThrow(new SSLHandshakeException("Invalid cert")); |
| 310 | } |
| 311 | |
| 312 | private void set302(HttpURLConnection connection, String location) throws IOException { |
| 313 | setStatus(connection, 302); |
| 314 | when(connection.getHeaderField(LOCATION_HEADER)).thenReturn(location); |
| 315 | } |
| 316 | |
| 317 | private void setPortal302(HttpURLConnection connection) throws IOException { |
| 318 | set302(connection, "http://login.example.com"); |
| 319 | } |
| 320 | |
| 321 | private void setStatus(HttpURLConnection connection, int status) throws IOException { |
| 322 | when(connection.getResponseCode()).thenReturn(status); |
Hugo Benichi | c894b12 | 2017-07-31 12:58:20 +0900 | [diff] [blame] | 323 | } |
| 324 | } |
| 325 | |