• 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.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