diff options
author | Remi NGUYEN VAN <reminv@google.com> | 2019-12-09 16:40:02 +0900 |
---|---|---|
committer | Remi NGUYEN VAN <reminv@google.com> | 2020-02-15 05:53:09 +0900 |
commit | 2fa48c2836435cb332592d50eb1d1f83b3715ce4 (patch) | |
tree | 00991391b130ce9444438821c54a531fdc3de551 /src | |
parent | 8282d3c8f1c152f171099b617341f89444a2d92d (diff) |
Add CaptivePortalData fetching to NetworkMonitor
When a capport API URL is sent through notifyNetworkConnected or
notifyLinkPropertiesChanged, NetworkMonitor will (re)evaluate to
fetch the data, and report it back via notifyCaptivePortalDataChanged.
Test: atest NetworkStackTests NetworkStackNextTests
Bug: 139269711
Change-Id: I101e2b2799d399fa7b560bf5ea3d86dac7ba1be9
Diffstat (limited to 'src')
-rw-r--r-- | src/com/android/server/connectivity/NetworkMonitor.java | 237 |
1 files changed, 200 insertions, 37 deletions
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java index 9ee4cd9..c29cea4 100644 --- a/src/com/android/server/connectivity/NetworkMonitor.java +++ b/src/com/android/server/connectivity/NetworkMonitor.java @@ -139,19 +139,26 @@ import androidx.annotation.BoolRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import androidx.annotation.VisibleForTesting; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.RingBufferIndices; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.internal.util.TrafficStatsConstants; import com.android.networkstack.R; +import com.android.networkstack.apishim.CaptivePortalDataShim; +import com.android.networkstack.apishim.CaptivePortalDataShimImpl; +import com.android.networkstack.apishim.NetworkInformationShimImpl; import com.android.networkstack.apishim.ShimUtils; +import com.android.networkstack.apishim.UnsupportedApiLevelException; import com.android.networkstack.metrics.DataStallDetectionStats; import com.android.networkstack.metrics.DataStallStatsUtils; import com.android.networkstack.netlink.TcpSocketTracker; import com.android.networkstack.util.DnsUtils; +import org.json.JSONException; +import org.json.JSONObject; + import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -169,6 +176,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Random; import java.util.StringJoiner; import java.util.UUID; @@ -197,6 +205,11 @@ public class NetworkMonitor extends StateMachine { private static final int SOCKET_TIMEOUT_MS = 10000; private static final int PROBE_TIMEOUT_MS = 3000; + private static final int CAPPORT_API_MAX_JSON_LENGTH = 4096; + private static final String ACCEPT_HEADER = "Accept"; + private static final String CONTENT_TYPE_HEADER = "Content-Type"; + private static final String CAPPORT_API_CONTENT_TYPE = "application/captive+json"; + enum EvaluationResult { VALIDATED(true), CAPTIVE_PORTAL(false); @@ -760,7 +773,12 @@ public class NetworkMonitor extends StateMachine { maybeDisableHttpsProbing(true /* acceptPartial */); break; case EVENT_LINK_PROPERTIES_CHANGED: + final Uri oldCapportUrl = getCaptivePortalApiUrl(mLinkProperties); mLinkProperties = (LinkProperties) message.obj; + final Uri newCapportUrl = getCaptivePortalApiUrl(mLinkProperties); + if (!Objects.equals(oldCapportUrl, newCapportUrl)) { + sendMessage(CMD_FORCE_REEVALUATION, NO_UID, 0); + } break; case EVENT_NETWORK_CAPABILITIES_CHANGED: mNetworkCapabilities = (NetworkCapabilities) message.obj; @@ -964,6 +982,8 @@ public class NetworkMonitor extends StateMachine { // Being in the EvaluatingState State indicates the Network is being evaluated for internet // connectivity, or that the user has indicated that this network is unwanted. private class EvaluatingState extends State { + private Uri mEvaluatingCapportUrl; + @Override public void enter() { // If we have already started to track time spent in EvaluatingState @@ -979,6 +999,7 @@ public class NetworkMonitor extends StateMachine { } mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS; mEvaluateAttempts = 0; + mEvaluatingCapportUrl = getCaptivePortalApiUrl(mLinkProperties); // Reset all current probe results to zero, but retain current validation state until // validation succeeds or fails. mEvaluationState.clearProbeResults(); @@ -1029,10 +1050,8 @@ public class NetworkMonitor extends StateMachine { transitionTo(mProbingState); return HANDLED; case CMD_FORCE_REEVALUATION: - // Before IGNORE_REEVALUATE_ATTEMPTS attempts are made, - // ignore any re-evaluation requests. After, restart the - // evaluation process via EvaluatingState#enter. - return (mEvaluateAttempts < IGNORE_REEVALUATE_ATTEMPTS) ? HANDLED : NOT_HANDLED; + // The evaluation process restarts via EvaluatingState#enter. + return shouldAcceptForceRevalidation() ? NOT_HANDLED : HANDLED; // Disable HTTPS probe and transition to EvaluatingPrivateDnsState because: // 1. Network is connected and finish the network validation. // 2. NetworkMonitor detects network is partial connectivity and user accepts it. @@ -1045,6 +1064,15 @@ public class NetworkMonitor extends StateMachine { } } + private boolean shouldAcceptForceRevalidation() { + // If the captive portal URL has changed since the last evaluation attempt, always + // revalidate. Otherwise, ignore any re-evaluation requests before + // IGNORE_REEVALUATE_ATTEMPTS are made. + return mEvaluateAttempts >= IGNORE_REEVALUATE_ATTEMPTS + || !Objects.equals( + mEvaluatingCapportUrl, getCaptivePortalApiUrl(mLinkProperties)); + } + @Override public void exit() { TrafficStats.clearThreadStatsUid(); @@ -1942,45 +1970,161 @@ public class NetworkMonitor extends StateMachine { } } - private CaptivePortalProbeResult sendParallelHttpProbes( - ProxyInfo proxy, URL httpsUrl, URL httpUrl) { - // Number of probes to wait for. If a probe completes with a conclusive answer - // it shortcuts the latch immediately by forcing the count to 0. - final CountDownLatch latch = new CountDownLatch(2); + private abstract static class ProbeThread extends Thread { + private final CountDownLatch mLatch; + private final ProxyInfo mProxy; + private final URL mUrl; + protected final Uri mCaptivePortalApiUrl; - final class ProbeThread extends Thread { - private final boolean mIsHttps; - private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED; + protected ProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, + Uri captivePortalApiUrl) { + mLatch = latch; + mProxy = proxy; + mUrl = url; + mCaptivePortalApiUrl = captivePortalApiUrl; + } - ProbeThread(boolean isHttps) { - mIsHttps = isHttps; - } + private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED; - public CaptivePortalProbeResult result() { - return mResult; + public CaptivePortalProbeResult result() { + return mResult; + } + + protected abstract CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url); + public abstract boolean isConclusiveResult(CaptivePortalProbeResult result); + + @Override + public void run() { + mResult = sendProbe(mProxy, mUrl); + if (isConclusiveResult(mResult)) { + // Stop waiting immediately if any probe is conclusive. + while (mLatch.getCount() > 0) { + mLatch.countDown(); + } } + // Signal this probe has completed. + mLatch.countDown(); + } + } - @Override - public void run() { - if (mIsHttps) { - mResult = - sendDnsAndHttpProbes(proxy, httpsUrl, ValidationProbeEvent.PROBE_HTTPS); - } else { - mResult = sendDnsAndHttpProbes(proxy, httpUrl, ValidationProbeEvent.PROBE_HTTP); + final class HttpsProbeThread extends ProbeThread { + HttpsProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { + super(latch, proxy, url, captivePortalApiUrl); + } + + @Override + protected CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url) { + return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTPS); + } + + @Override + public boolean isConclusiveResult(CaptivePortalProbeResult result) { + // isPortal() is not expected on the HTTPS probe, but check it nonetheless. + // In case the capport API is available, the API is authoritative on whether there is + // a portal, so the HTTPS probe is not enough to conclude there is connectivity, + // and a determination will be made once the capport API probe returns. Note that the + // API can only force the system to detect a portal even if the HTTPS probe succeeds. + // It cannot force the system to detect no portal if the HTTPS probe fails. + return (result.isPortal() || result.isSuccessful()) && mCaptivePortalApiUrl == null; + } + } + + final class HttpProbeThread extends ProbeThread { + private volatile CaptivePortalDataShim mCapportData; + HttpProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { + super(latch, proxy, url, captivePortalApiUrl); + } + + CaptivePortalDataShim getCaptivePortalData() { + return mCapportData; + } + + private CaptivePortalDataShim tryCapportApiProbe() { + if (mCaptivePortalApiUrl == null) return null; + validationLog("Fetching captive portal data from " + mCaptivePortalApiUrl); + + final String apiContent; + try { + final URL url = new URL(mCaptivePortalApiUrl.toString()); + if (!"https".equals(url.getProtocol())) { + validationLog("Invalid captive portal API protocol: " + url.getProtocol()); + return null; } - if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) { - // Stop waiting immediately if https succeeds or if http finds a portal. - while (latch.getCount() > 0) { - latch.countDown(); - } + + final HttpURLConnection conn = makeProbeConnection( + url, true /* followRedirects */); + conn.setRequestProperty(ACCEPT_HEADER, CAPPORT_API_CONTENT_TYPE); + final int responseCode = conn.getResponseCode(); + if (responseCode != 200) { + validationLog("Non-200 API response code: " + conn.getResponseCode()); + return null; + } + final Charset charset = extractCharset(conn.getHeaderField(CONTENT_TYPE_HEADER)); + if (charset != StandardCharsets.UTF_8) { + validationLog("Invalid charset for capport API: " + charset); + return null; } - // Signal this probe has completed. - latch.countDown(); + + apiContent = readAsString(conn.getInputStream(), + CAPPORT_API_MAX_JSON_LENGTH, charset); + } catch (IOException e) { + validationLog("I/O error reading capport data: " + e.getMessage()); + return null; + } + + try { + final JSONObject info = new JSONObject(apiContent); + return CaptivePortalDataShimImpl.fromJson(info); + } catch (JSONException e) { + validationLog("Could not parse capport API JSON: " + e.getMessage()); + return null; + } catch (UnsupportedApiLevelException e) { + validationLog("Platform API too low to support capport API"); + return null; } } - final ProbeThread httpsProbe = new ProbeThread(true); - final ProbeThread httpProbe = new ProbeThread(false); + @Override + protected CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url) { + mCapportData = tryCapportApiProbe(); + if (mCapportData != null && mCapportData.isCaptive()) { + if (mCapportData.getUserPortalUrl() == null) { + validationLog("Missing user-portal-url from capport response"); + return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTP); + } + final String loginUrlString = mCapportData.getUserPortalUrl().toString(); + // Starting from R (where CaptivePortalData was introduced), the captive portal app + // delegates to NetworkMonitor for verifying when the network validates instead of + // probing the detectUrl. So pass the detectUrl to have the portal open on that, + // page; CaptivePortalLogin will not use it for probing. + return new CaptivePortalProbeResult( + CaptivePortalProbeResult.PORTAL_CODE, + loginUrlString /* redirectUrl */, + loginUrlString /* detectUrl */); + } + + // If the API says it's not captive, still check for HTTP connectivity. This helps + // with partial connectivity detection, and a broken API saying that there is no + // redirect when there is one. + return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTP); + } + + @Override + public boolean isConclusiveResult(CaptivePortalProbeResult result) { + return result.isPortal(); + } + } + + private CaptivePortalProbeResult sendParallelHttpProbes( + ProxyInfo proxy, URL httpsUrl, URL httpUrl) { + // Number of probes to wait for. If a probe completes with a conclusive answer + // it shortcuts the latch immediately by forcing the count to 0. + final CountDownLatch latch = new CountDownLatch(2); + + final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties); + final HttpsProbeThread httpsProbe = new HttpsProbeThread(latch, proxy, httpsUrl, + capportApiUrl); + final HttpProbeThread httpProbe = new HttpProbeThread(latch, proxy, httpUrl, capportApiUrl); try { httpsProbe.start(); @@ -1995,12 +2139,14 @@ public class NetworkMonitor extends StateMachine { final CaptivePortalProbeResult httpResult = httpProbe.result(); // Look for a conclusive probe result first. - if (httpResult.isPortal()) { + if (httpProbe.isConclusiveResult(httpResult)) { + maybeReportCaptivePortalData(httpProbe.getCaptivePortalData()); reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, httpResult); return httpResult; } - // httpsResult.isPortal() is not expected, but check it nonetheless. - if (httpsResult.isPortal() || httpsResult.isSuccessful()) { + + if (httpsProbe.isConclusiveResult(httpsResult)) { + maybeReportCaptivePortalData(httpProbe.getCaptivePortalData()); reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTPS, httpsResult); return httpsResult; } @@ -2019,6 +2165,7 @@ public class NetworkMonitor extends StateMachine { // Otherwise wait until http and https probes completes and use their results. try { httpProbe.join(); + maybeReportCaptivePortalData(httpProbe.getCaptivePortalData()); reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, httpProbe.result()); if (httpProbe.result().isPortal()) { @@ -2519,6 +2666,18 @@ public class NetworkMonitor extends StateMachine { mEvaluationState.noteProbeResult(probeResult, succeeded); } + private void maybeReportCaptivePortalData(@Nullable CaptivePortalDataShim data) { + // Do not clear data even if it is null: access points should not stop serving the API, so + // if the API disappears this is treated as a temporary failure, and previous data should + // remain valid. + if (data == null) return; + try { + data.notifyChanged(mCallback); + } catch (RemoteException e) { + Log.e(TAG, "Error notifying ConnectivityService of new capport data", e); + } + } + /** * Interface for logging dns results. */ @@ -2546,4 +2705,8 @@ public class NetworkMonitor extends StateMachine { return ((type & DATA_STALL_EVALUATION_TYPE_DNS) != 0) ? new DnsStallDetector(threshold) : null; } + + private static Uri getCaptivePortalApiUrl(LinkProperties lp) { + return NetworkInformationShimImpl.newInstance().getCaptivePortalApiUrl(lp); + } } |