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.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTPS_URL
48 import com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL
49 import com.android.testutils.AutoReleaseNetworkCallbackRule
50 import com.android.testutils.DeviceConfigRule
51 import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
52 import com.android.testutils.SkipMainlinePresubmit
53 import com.android.testutils.TestHttpServer
54 import com.android.testutils.TestHttpServer.Request
55 import com.android.testutils.TestableNetworkCallback
56 import com.android.testutils.runAsShell
57 import fi.iki.elonen.NanoHTTPD.Response.Status
58 import java.util.concurrent.CompletableFuture
59 import java.util.concurrent.TimeUnit
60 import java.util.concurrent.TimeoutException
61 import junit.framework.AssertionFailedError
62 import kotlin.test.Test
63 import kotlin.test.assertNotEquals
64 import kotlin.test.assertNotNull
65 import kotlin.test.assertTrue
66 import org.junit.After
67 import org.junit.Assume.assumeFalse
68 import org.junit.Assume.assumeTrue
69 import org.junit.Before
70 import org.junit.BeforeClass
71 import org.junit.Rule
72 import org.junit.runner.RunWith
73
74 private const val TEST_HTTPS_URL_PATH = "/https_path"
75 private const val TEST_HTTP_URL_PATH = "/http_path"
76 private const val TEST_PORTAL_URL_PATH = "/portal_path"
77
78 private const val LOCALHOST_HOSTNAME = "localhost"
79
80 // Re-connecting to the AP, obtaining an IP address, revalidating can take a long time
81 private const val WIFI_CONNECT_TIMEOUT_MS = 40_000L
82 private const val TEST_TIMEOUT_MS = 20_000L
83
84 private const val TAG = "CaptivePortalTest"
85
assertGetnull86 private fun <T> CompletableFuture<T>.assertGet(timeoutMs: Long, message: String): T {
87 try {
88 return get(timeoutMs, TimeUnit.MILLISECONDS)
89 } catch (e: TimeoutException) {
90 throw AssertionFailedError(message)
91 }
92 }
93
94 @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
95 @RunWith(AndroidJUnit4::class)
96 class CaptivePortalTest {
<lambda>null97 private val context: android.content.Context by lazy { getInstrumentation().context }
<lambda>null98 private val cm by lazy { context.getSystemService(ConnectivityManager::class.java)!! }
<lambda>null99 private val pm by lazy { context.packageManager }
<lambda>null100 private val utils by lazy { CtsNetUtils(context) }
101
102 private val server = TestHttpServer("localhost")
103
104 @get:Rule(order = 1)
105 val deviceConfigRule = DeviceConfigRule(retryCountBeforeSIfConfigChanged = 5)
106
107 @get:Rule(order = 2)
108 val networkCallbackRule = AutoReleaseNetworkCallbackRule()
109
110 companion object {
111 @JvmStatic @BeforeClass
setUpClassnull112 fun setUpClass() {
113 runAsShell(READ_DEVICE_CONFIG) {
114 // Verify that the test URLs are not normally set on the device, but do not fail if
115 // the test URLs are set to what this test uses (URLs on localhost), in case the
116 // test was interrupted manually and rerun.
117 assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
118 assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
119 }
120 NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig()
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
130 @Before
setUpnull131 fun setUp() {
132 server.start()
133 }
134
135 @After
tearDownnull136 fun tearDown() {
137 if (pm.hasSystemFeature(FEATURE_WIFI)) {
138 deviceConfigRule.runAfterNextCleanup { reconnectWifi() }
139 }
140 server.stop()
141 }
142
143 @Test
144 @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
testCaptivePortalIsNotDefaultNetworknull145 fun testCaptivePortalIsNotDefaultNetwork() {
146 assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
147 assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
148 assumeFalse(pm.hasSystemFeature(FEATURE_WATCH))
149 utils.ensureWifiConnected()
150 val cellNetwork = networkCallbackRule.requestCell()
151
152 // Verify cell network is validated
153 val cellReq = NetworkRequest.Builder()
154 .addTransportType(TRANSPORT_CELLULAR)
155 .addCapability(NET_CAPABILITY_INTERNET)
156 .build()
157 val cellCb = networkCallbackRule.registerNetworkCallback(cellReq,
158 TestableNetworkCallback(timeoutMs = TEST_TIMEOUT_MS))
159 val cb = cellCb.poll { it.network == cellNetwork &&
160 it is CapabilitiesChanged && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
161 }
162 assertNotNull(cb, "Mobile network $cellNetwork has no access to the internet. " +
163 "Check the mobile data connection.")
164
165 // Have network validation use a local server that serves a HTTPS error / HTTP redirect
166 server.addResponse(Request(TEST_PORTAL_URL_PATH), Status.OK,
167 content = "Test captive portal content")
168 server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
169 val headers = mapOf("Location" to makeUrl(TEST_PORTAL_URL_PATH))
170 server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT, headers)
171 setHttpsUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTPS_URL_PATH))
172 setHttpUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTP_URL_PATH))
173 Log.d(TAG, "Set portal URLs to $TEST_HTTPS_URL_PATH and $TEST_HTTP_URL_PATH")
174 // URL expiration needs to be in the next 10 minutes
175 assertTrue(WIFI_CONNECT_TIMEOUT_MS < TimeUnit.MINUTES.toMillis(10))
176 setUrlExpirationDeviceConfig(deviceConfigRule,
177 System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
178
179 // Wait for a captive portal to be detected on the network
180 val wifiNetworkFuture = CompletableFuture<Network>()
181 val wifiCb = object : NetworkCallback() {
182 override fun onCapabilitiesChanged(
183 network: Network,
184 nc: NetworkCapabilities
185 ) {
186 if (nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) {
187 wifiNetworkFuture.complete(network)
188 }
189 }
190 }
191 cm.requestNetwork(NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(), wifiCb)
192
193 try {
194 reconnectWifi()
195 val network = wifiNetworkFuture.assertGet(WIFI_CONNECT_TIMEOUT_MS,
196 "Captive portal not detected after ${WIFI_CONNECT_TIMEOUT_MS}ms")
197
198 val wifiDefaultMessage = "Wifi should not be the default network when a captive " +
199 "portal was detected and another network (mobile data) can provide internet " +
200 "access."
201 assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
202
203 runAsShell(NETWORK_SETTINGS) { cm.startCaptivePortalApp(network) }
204
205 // Expect the portal content to be fetched at some point after detecting the portal.
206 // Some implementations may fetch the URL before startCaptivePortalApp is called.
207 assertNotNull(server.requestsRecord.poll(TEST_TIMEOUT_MS, pos = 0) {
208 it.path == TEST_PORTAL_URL_PATH
209 }, "The captive portal login page was still not fetched ${TEST_TIMEOUT_MS}ms " +
210 "after startCaptivePortalApp.")
211
212 assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
213 } finally {
214 cm.unregisterNetworkCallback(wifiCb)
215 server.stop()
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