• 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.os.Build
44 import android.platform.test.annotations.AppModeFull
45 import android.provider.DeviceConfig
46 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
47 import android.text.TextUtils
48 import android.util.Log
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 = 40_000L
80 private const val TEST_TIMEOUT_MS = 20_000L
81 
82 private const val TAG = "CaptivePortalTest"
83 
assertGetnull84 private fun <T> CompletableFuture<T>.assertGet(timeoutMs: Long, message: String): T {
85     try {
86         return get(timeoutMs, TimeUnit.MILLISECONDS)
87     } catch (e: TimeoutException) {
88         throw AssertionFailedError(message)
89     }
90 }
91 
92 @AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
93 @RunWith(AndroidJUnit4::class)
94 class CaptivePortalTest {
<lambda>null95     private val context: android.content.Context by lazy { getInstrumentation().context }
<lambda>null96     private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
<lambda>null97     private val pm by lazy { context.packageManager }
<lambda>null98     private val utils by lazy { CtsNetUtils(context) }
99 
100     private val server = TestHttpServer("localhost")
101 
102     @Before
setUpnull103     fun setUp() {
104         runAsShell(READ_DEVICE_CONFIG) {
105             // Verify that the test URLs are not normally set on the device, but do not fail if the
106             // test URLs are set to what this test uses (URLs on localhost), in case the test was
107             // interrupted manually and rerun.
108             assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
109             assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
110         }
111         clearValidationTestUrlsDeviceConfig()
112         server.start()
113     }
114 
115     @After
tearDownnull116     fun tearDown() {
117         clearValidationTestUrlsDeviceConfig()
118         if (pm.hasSystemFeature(FEATURE_WIFI)) {
119             reconnectWifi()
120         }
121         server.stop()
122     }
123 
assertEmptyOrLocalhostUrlnull124     private fun assertEmptyOrLocalhostUrl(urlKey: String) {
125         val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
126         assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
127                 "$urlKey must not be set in production scenarios (current value: $url)")
128     }
129 
130     @Test
testCaptivePortalIsNotDefaultNetworknull131     fun testCaptivePortalIsNotDefaultNetwork() {
132         assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
133         assumeTrue(pm.hasSystemFeature(FEATURE_WIFI))
134         assumeFalse(pm.hasSystemFeature(FEATURE_WATCH))
135         utils.ensureWifiConnected()
136         val cellNetwork = utils.connectToCell()
137 
138         // Verify cell network is validated
139         val cellReq = NetworkRequest.Builder()
140                 .addTransportType(TRANSPORT_CELLULAR)
141                 .addCapability(NET_CAPABILITY_INTERNET)
142                 .build()
143         val cellCb = TestableNetworkCallback(timeoutMs = TEST_TIMEOUT_MS)
144         cm.registerNetworkCallback(cellReq, cellCb)
145         val cb = cellCb.eventuallyExpectOrNull<RecorderCallback.CallbackEntry.CapabilitiesChanged> {
146             it.network == cellNetwork && it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
147         }
148         assertNotNull(cb, "Mobile network $cellNetwork has no access to the internet. " +
149                 "Check the mobile data connection.")
150 
151         // Have network validation use a local server that serves a HTTPS error / HTTP redirect
152         server.addResponse(Request(TEST_PORTAL_URL_PATH), Status.OK,
153                 content = "Test captive portal content")
154         server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
155         val headers = mapOf("Location" to makeUrl(TEST_PORTAL_URL_PATH))
156         server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT, headers)
157         setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH))
158         setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH))
159         Log.d(TAG, "Set portal URLs to $TEST_HTTPS_URL_PATH and $TEST_HTTP_URL_PATH")
160         // URL expiration needs to be in the next 10 minutes
161         assertTrue(WIFI_CONNECT_TIMEOUT_MS < TimeUnit.MINUTES.toMillis(10))
162         setUrlExpirationDeviceConfig(System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
163 
164         // Wait for a captive portal to be detected on the network
165         val wifiNetworkFuture = CompletableFuture<Network>()
166         val wifiCb = object : NetworkCallback() {
167             override fun onCapabilitiesChanged(
168                 network: Network,
169                 nc: NetworkCapabilities
170             ) {
171                 if (nc.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) {
172                     wifiNetworkFuture.complete(network)
173                 }
174             }
175         }
176         cm.requestNetwork(NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI).build(), wifiCb)
177 
178         try {
179             reconnectWifi()
180             val network = wifiNetworkFuture.assertGet(WIFI_CONNECT_TIMEOUT_MS,
181                     "Captive portal not detected after ${WIFI_CONNECT_TIMEOUT_MS}ms")
182 
183             val wifiDefaultMessage = "Wifi should not be the default network when a captive " +
184                     "portal was detected and another network (mobile data) can provide internet " +
185                     "access."
186             assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
187 
188             val startPortalAppPermission =
189                     if (isDevSdkInRange(0, Build.VERSION_CODES.Q)) CONNECTIVITY_INTERNAL
190                     else NETWORK_SETTINGS
191             runAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) }
192 
193             // Expect the portal content to be fetched at some point after detecting the portal.
194             // Some implementations may fetch the URL before startCaptivePortalApp is called.
195             assertNotNull(server.requestsRecord.poll(TEST_TIMEOUT_MS, pos = 0) {
196                 it.path == TEST_PORTAL_URL_PATH
197             }, "The captive portal login page was still not fetched ${TEST_TIMEOUT_MS}ms " +
198                     "after startCaptivePortalApp.")
199 
200             assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
201         } finally {
202             cm.unregisterNetworkCallback(wifiCb)
203             server.stop()
204             // disconnectFromCell should be called after connectToCell
205             utils.disconnectFromCell()
206         }
207     }
208 
209     /**
210      * Create a URL string that, when fetched, will hit the test server with the given URL [path].
211      */
makeUrlnull212     private fun makeUrl(path: String) = "http://localhost:${server.listeningPort}" + path
213 
214     private fun reconnectWifi() {
215         utils.ensureWifiDisconnected(null /* wifiNetworkToCheck */)
216         utils.ensureWifiConnected()
217     }
218 }