• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 android.net.cts
18 
19 import android.Manifest.permission.CONNECTIVITY_INTERNAL
20 import android.Manifest.permission.NETWORK_SETTINGS
21 import android.Manifest.permission.READ_DEVICE_CONFIG
22 import android.content.pm.PackageManager.FEATURE_TELEPHONY
23 import android.content.pm.PackageManager.FEATURE_WATCH
24 import android.content.pm.PackageManager.FEATURE_WIFI
25 import android.net.ConnectivityManager
26 import android.net.ConnectivityManager.NetworkCallback
27 import android.net.Network
28 import android.net.NetworkCapabilities
29 import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
30 import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
31 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
32 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
33 import android.net.NetworkCapabilities.TRANSPORT_WIFI
34 import android.net.NetworkRequest
35 import android.net.Uri
36 import android.net.cts.NetworkValidationTestUtil.setHttpUrlDeviceConfig
37 import android.net.cts.NetworkValidationTestUtil.setHttpsUrlDeviceConfig
38 import android.net.cts.NetworkValidationTestUtil.setUrlExpirationDeviceConfig
39 import android.net.cts.util.CtsNetUtils
40 import android.platform.test.annotations.AppModeFull
41 import android.provider.DeviceConfig
42 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
43 import android.text.TextUtils
44 import android.util.Log
45 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
46 import androidx.test.runner.AndroidJUnit4
47 import com.android.modules.utils.build.SdkLevel.isAtLeastR
48 import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL
49 import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL
50 import com.android.testutils.DeviceConfigRule
51 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
52 import com.android.testutils.TestHttpServer
53 import com.android.testutils.TestHttpServer.Request
54 import com.android.testutils.TestableNetworkCallback
55 import com.android.testutils.runAsShell
56 import fi.iki.elonen.NanoHTTPD.Response.Status
57 import java.util.concurrent.CompletableFuture
58 import java.util.concurrent.TimeUnit
59 import java.util.concurrent.TimeoutException
60 import junit.framework.AssertionFailedError
61 import kotlin.test.Test
62 import kotlin.test.assertNotEquals
63 import kotlin.test.assertNotNull
64 import kotlin.test.assertTrue
65 import org.junit.After
66 import org.junit.Assume.assumeFalse
67 import org.junit.Assume.assumeTrue
68 import org.junit.Before
69 import org.junit.BeforeClass
70 import org.junit.Rule
71 import org.junit.runner.RunWith
72 
73 private const val TEST_HTTPS_URL_PATH = "/https_path"
74 private const val TEST_HTTP_URL_PATH = "/http_path"
75 private const val TEST_PORTAL_URL_PATH = "/portal_path"
76 
77 private const val LOCALHOST_HOSTNAME = "localhost"
78 
79 // Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
80 private const val WIFI_CONNECT_TIMEOUT_MS = 40_000L
81 private const val TEST_TIMEOUT_MS = 20_000L
82 
83 private const val TAG = "CaptivePortalTest"
84 
assertGetnull85 private fun <T> CompletableFuture<T>.assertGet(timeoutMs: Long, message: String): T {
86     try {
87         return get(timeoutMs, TimeUnit.MILLISECONDS)
88     } catch (e: TimeoutException) {
89         throw AssertionFailedError(message)
90     }
91 }
92 
93 @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
94 @RunWith(AndroidJUnit4::class)
95 class CaptivePortalTest {
<lambda>null96     private val context: android.content.Context by lazy { getInstrumentation().context }
<lambda>null97     private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
<lambda>null98     private val pm by lazy { context.packageManager }
<lambda>null99     private val utils by lazy { CtsNetUtils(context) }
100 
101     private val server = TestHttpServer("localhost")
102 
103     @get:Rule
104     val deviceConfigRule = DeviceConfigRule(retryCountBeforeSIfConfigChanged = 5)
105 
106     companion object {
107         @JvmStatic @BeforeClass
setUpClassnull108         fun setUpClass() {
109             runAsShell(READ_DEVICE_CONFIG) {
110                 // Verify that the test URLs are not normally set on the device, but do not fail if
111                 // the test URLs are set to what this test uses (URLs on localhost), in case the
112                 // test was interrupted manually and rerun.
113                 assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
114                 assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
115             }
116             NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig()
117         }
118 
assertEmptyOrLocalhostUrlnull119         private fun assertEmptyOrLocalhostUrl(urlKey: String) {
120             val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
121             assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
122                     "$urlKey must not be set in production scenarios (current value: $url)")
123         }
124     }
125 
126     @Before
setUpnull127     fun setUp() {
128         server.start()
129     }
130 
131     @After
tearDownnull132     fun tearDown() {
133         if (pm.hasSystemFeature(FEATURE_WIFI)) {
134             deviceConfigRule.runAfterNextCleanup { reconnectWifi() }
135         }
136         server.stop()
137     }
138 
139     @Test
testCaptivePortalIsNotDefaultNetworknull140     fun testCaptivePortalIsNotDefaultNetwork() {
141         assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
142         assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
143         assumeFalse(pm.hasSystemFeature(FEATURE_WATCH))
144         utils.ensureWifiConnected()
145         val cellNetwork = utils.connectToCell()
146 
147         // Verify cell network is validated
148         val cellReq = NetworkRequest.Builder()
149                 .addTransportType(TRANSPORT_CELLULAR)
150                 .addCapability(NET_CAPABILITY_INTERNET)
151                 .build()
152         val cellCb = TestableNetworkCallback(timeoutMs = TEST_TIMEOUT_MS)
153         cm.registerNetworkCallback(cellReq, cellCb)
154         val cb = cellCb.poll { it.network == cellNetwork &&
155                 it is CapabilitiesChanged && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
156         }
157         assertNotNull(cb, "Mobile network $cellNetwork has no access to the internet. " +
158                 "Check the mobile data connection.")
159 
160         // Have network validation use a local server that serves a HTTPS error / HTTP redirect
161         server.addResponse(Request(TEST_PORTAL_URL_PATH), Status.OK,
162                 content = "Test captive portal content")
163         server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
164         val headers = mapOf("Location" to makeUrl(TEST_PORTAL_URL_PATH))
165         server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT, headers)
166         setHttpsUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTPS_URL_PATH))
167         setHttpUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTP_URL_PATH))
168         Log.d(TAG, "Set portal URLs to $TEST_HTTPS_URL_PATH and $TEST_HTTP_URL_PATH")
169         // URL expiration needs to be in the next 10 minutes
170         assertTrue(WIFI_CONNECT_TIMEOUT_MS < TimeUnit.MINUTES.toMillis(10))
171         setUrlExpirationDeviceConfig(deviceConfigRule,
172                 System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
173 
174         // Wait for a captive portal to be detected on the network
175         val wifiNetworkFuture = CompletableFuture<Network>()
176         val wifiCb = object : NetworkCallback() {
177             override fun onCapabilitiesChanged(
178                 network: Network,
179                 nc: NetworkCapabilities
180             ) {
181                 if (nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) {
182                     wifiNetworkFuture.complete(network)
183                 }
184             }
185         }
186         cm.requestNetwork(NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(), wifiCb)
187 
188         try {
189             reconnectWifi()
190             val network = wifiNetworkFuture.assertGet(WIFI_CONNECT_TIMEOUT_MS,
191                     "Captive portal not detected after ${WIFI_CONNECT_TIMEOUT_MS}ms")
192 
193             val wifiDefaultMessage = "Wifi should not be the default network when a captive " +
194                     "portal was detected and another network (mobile data) can provide internet " +
195                     "access."
196             assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
197 
198             val startPortalAppPermission =
199                     if (isAtLeastR()) NETWORK_SETTINGS
200                     else CONNECTIVITY_INTERNAL
201             runAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) }
202 
203             // Expect the portal content to be fetched at some point after detecting the portal.
204             // Some implementations may fetch the URL before startCaptivePortalApp is called.
205             assertNotNull(server.requestsRecord.poll(TEST_TIMEOUT_MS, pos = 0) {
206                 it.path == TEST_PORTAL_URL_PATH
207             }, "The captive portal login page was still not fetched ${TEST_TIMEOUT_MS}ms " +
208                     "after startCaptivePortalApp.")
209 
210             assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
211         } finally {
212             cm.unregisterNetworkCallback(wifiCb)
213             server.stop()
214             // disconnectFromCell should be called after connectToCell
215             utils.disconnectFromCell()
216         }
217     }
218 
219     /**
220      * Create a URL string that, when fetched, will hit the test server with the given URL [path].
221      */
makeUrlnull222     private fun makeUrl(path: String) = "http://localhost:${server.listeningPort}" + path
223 
224     private fun reconnectWifi() {
225         utils.ensureWifiDisconnected(null /* wifiNetworkToCheck */)
226         utils.ensureWifiConnected()
227     }
228 }
229