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