Skip to content
This repository was archived by the owner on Nov 8, 2023. It is now read-only.

Commit c5be12e

Browse files
committedApr 28, 2016
Make isCaptivePortal perform both HTTP and HTTPS probes.
Also a couple of minor cleanups and logging tweaks. Bug: 26075613 Change-Id: I67b09e96d72764179339b616072bb2ce06aabf33
1 parent 7461032 commit c5be12e

File tree

4 files changed

+227
-65
lines changed

4 files changed

+227
-65
lines changed
 

‎api/system-current.txt

+6-2
Original file line numberDiff line numberDiff line change
@@ -26170,8 +26170,12 @@ package android.net.metrics {
2617026170
method public static void logEvent(int, long, int, int);
2617126171
method public void writeToParcel(android.os.Parcel, int);
2617226172
field public static final android.os.Parcelable.Creator<android.net.metrics.ValidationProbeEvent> CREATOR;
26173-
field public static final int PROBE_HTTP = 0; // 0x0
26174-
field public static final int PROBE_HTTPS = 1; // 0x1
26173+
field public static final int DNS_FAILURE = 0; // 0x0
26174+
field public static final int DNS_SUCCESS = 1; // 0x1
26175+
field public static final int PROBE_DNS = 0; // 0x0
26176+
field public static final int PROBE_HTTP = 1; // 0x1
26177+
field public static final int PROBE_HTTPS = 2; // 0x2
26178+
field public static final int PROBE_PAC = 3; // 0x3
2617526179
field public final long durationMs;
2617626180
field public final int netId;
2617726181
field public final int probeType;

‎core/java/android/net/metrics/ValidationProbeEvent.java

+13-3
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@
2929
@SystemApi
3030
public final class ValidationProbeEvent extends IpConnectivityEvent implements Parcelable {
3131

32-
public static final int PROBE_HTTP = 0;
33-
public static final int PROBE_HTTPS = 1;
32+
public static final int PROBE_DNS = 0;
33+
public static final int PROBE_HTTP = 1;
34+
public static final int PROBE_HTTPS = 2;
35+
public static final int PROBE_PAC = 3;
36+
37+
public static final int DNS_FAILURE = 0;
38+
public static final int DNS_SUCCESS = 1;
3439

3540
public final int netId;
3641
public final long durationMs;
@@ -73,14 +78,19 @@ public ValidationProbeEvent[] newArray(int size) {
7378
}
7479
};
7580

81+
/** @hide */
82+
public static String getProbeName(int probeType) {
83+
return Decoder.constants.get(probeType, "PROBE_???");
84+
}
85+
7686
public static void logEvent(int netId, long durationMs, int probeType, int returnCode) {
7787
logEvent(new ValidationProbeEvent(netId, durationMs, probeType, returnCode));
7888
}
7989

8090
@Override
8191
public String toString() {
8292
return String.format("ValidationProbeEvent(%d, %s:%d, %dms)",
83-
netId, Decoder.constants.get(probeType), returnCode, durationMs);
93+
netId, getProbeName(probeType), returnCode, durationMs);
8494
}
8595

8696
final static class Decoder {

‎core/java/android/provider/Settings.java

+9
Original file line numberDiff line numberDiff line change
@@ -7705,6 +7705,15 @@ public static final class Global extends NameValueTable {
77057705
*/
77067706
public static final String CAPTIVE_PORTAL_SERVER = "captive_portal_server";
77077707

7708+
/**
7709+
* Whether to use HTTPS for network validation. This is enabled by default and the setting
7710+
* needs to be set to 0 to disable it. This setting is a misnomer because captive portals
7711+
* don't actually use HTTPS, but it's consistent with the other settings.
7712+
*
7713+
* @hide
7714+
*/
7715+
public static final String CAPTIVE_PORTAL_USE_HTTPS = "captive_portal_use_https";
7716+
77087717
/**
77097718
* Whether network service discovery is enabled.
77107719
*

‎services/core/java/com/android/server/connectivity/NetworkMonitor.java

+199-60
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@
7171
import java.io.IOException;
7272
import java.net.HttpURLConnection;
7373
import java.net.InetAddress;
74+
import java.net.MalformedURLException;
75+
import java.net.UnknownHostException;
7476
import java.net.URL;
77+
import java.util.concurrent.CountDownLatch;
78+
import java.util.concurrent.atomic.AtomicReference;
7579
import java.util.List;
7680
import java.util.Random;
7781

@@ -228,7 +232,8 @@ public class NetworkMonitor extends StateMachine {
228232
private final AlarmManager mAlarmManager;
229233
private final NetworkRequest mDefaultRequest;
230234

231-
private boolean mIsCaptivePortalCheckEnabled = false;
235+
private boolean mIsCaptivePortalCheckEnabled;
236+
private boolean mUseHttps;
232237

233238
// Set if the user explicitly selected "Do not use this network" in captive portal sign-in app.
234239
private boolean mUserDoesNotWant = false;
@@ -276,6 +281,8 @@ public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo network
276281

277282
mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
278283
Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
284+
mUseHttps = Settings.Global.getInt(mContext.getContentResolver(),
285+
Settings.Global.CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
279286

280287
start();
281288
}
@@ -324,6 +331,21 @@ public boolean processMessage(Message message) {
324331
return HANDLED;
325332
case CMD_CAPTIVE_PORTAL_APP_FINISHED:
326333
log("CaptivePortal App responded with " + message.arg1);
334+
335+
// If the user has seen and acted on a captive portal notification, and the
336+
// captive portal app is now closed, disable HTTPS probes. This avoids the
337+
// following pathological situation:
338+
//
339+
// 1. HTTP probe returns a captive portal, HTTPS probe fails or times out.
340+
// 2. User opens the app and logs into the captive portal.
341+
// 3. HTTP starts working, but HTTPS still doesn't work for some other reason -
342+
// perhaps due to the network blocking HTTPS?
343+
//
344+
// In this case, we'll fail to validate the network even after the app is
345+
// dismissed. There is now no way to use this network, because the app is now
346+
// gone, so the user cannot select "Use this network as is".
347+
mUseHttps = false;
348+
327349
switch (message.arg1) {
328350
case APP_RETURN_DISMISSED:
329351
sendMessage(CMD_FORCE_REEVALUATION, 0 /* no UID */, 0);
@@ -424,13 +446,20 @@ public void exit() {
424446
*/
425447
@VisibleForTesting
426448
public static final class CaptivePortalProbeResult {
449+
static final CaptivePortalProbeResult FAILED = new CaptivePortalProbeResult(599, null);
450+
427451
final int mHttpResponseCode; // HTTP response code returned from Internet probe.
428452
final String mRedirectUrl; // Redirect destination returned from Internet probe.
429453

430454
public CaptivePortalProbeResult(int httpResponseCode, String redirectUrl) {
431455
mHttpResponseCode = httpResponseCode;
432456
mRedirectUrl = redirectUrl;
433457
}
458+
459+
boolean isSuccessful() { return mHttpResponseCode == 204; }
460+
boolean isPortal() {
461+
return !isSuccessful() && mHttpResponseCode >= 200 && mHttpResponseCode <= 399;
462+
}
434463
}
435464

436465
// Being in the EvaluatingState State indicates the Network is being evaluated for internet
@@ -481,6 +510,7 @@ public boolean processMessage(Message message) {
481510
// expensive metered network, or unwanted leaking of the User Agent string.
482511
if (!mDefaultRequest.networkCapabilities.satisfiedByNetworkCapabilities(
483512
mNetworkAgentInfo.networkCapabilities)) {
513+
validationLog("Network would not satisfy default request, not validating");
484514
transitionTo(mValidatedState);
485515
return HANDLED;
486516
}
@@ -492,10 +522,9 @@ public boolean processMessage(Message message) {
492522
// will be unresponsive. isCaptivePortal() could be executed on another Thread
493523
// if this is found to cause problems.
494524
CaptivePortalProbeResult probeResult = isCaptivePortal();
495-
if (probeResult.mHttpResponseCode == 204) {
525+
if (probeResult.isSuccessful()) {
496526
transitionTo(mValidatedState);
497-
} else if (probeResult.mHttpResponseCode >= 200 &&
498-
probeResult.mHttpResponseCode <= 399) {
527+
} else if (probeResult.isPortal()) {
499528
mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
500529
NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.mRedirectUrl));
501530
transitionTo(mCaptivePortalState);
@@ -659,72 +688,127 @@ public void exit() {
659688
}
660689
}
661690

662-
public static String getCaptivePortalServerUrl(Context context) {
691+
private static String getCaptivePortalServerUrl(Context context, boolean isHttps) {
663692
String server = Settings.Global.getString(context.getContentResolver(),
664693
Settings.Global.CAPTIVE_PORTAL_SERVER);
665694
if (server == null) server = DEFAULT_SERVER;
666-
return "http://" + server + "/generate_204";
695+
return (isHttps ? "https" : "http") + "://" + server + "/generate_204";
696+
}
697+
698+
public static String getCaptivePortalServerUrl(Context context) {
699+
return getCaptivePortalServerUrl(context, false);
667700
}
668701

669-
/**
670-
* Do a URL fetch on a known server to see if we get the data we expect.
671-
* Returns HTTP response code.
672-
*/
673702
@VisibleForTesting
674703
protected CaptivePortalProbeResult isCaptivePortal() {
675704
if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204, null);
676705

677-
HttpURLConnection urlConnection = null;
678-
int httpResponseCode = 599;
679-
String redirectUrl = null;
680-
final Stopwatch probeTimer = new Stopwatch().start();
681-
try {
682-
URL url = new URL(getCaptivePortalServerUrl(mContext));
683-
// On networks with a PAC instead of fetching a URL that should result in a 204
684-
// response, we instead simply fetch the PAC script. This is done for a few reasons:
685-
// 1. At present our PAC code does not yet handle multiple PACs on multiple networks
686-
// until something like https://android-review.googlesource.com/#/c/115180/ lands.
687-
// Network.openConnection() will ignore network-specific PACs and instead fetch
688-
// using NO_PROXY. If a PAC is in place, the only fetch we know will succeed with
689-
// NO_PROXY is the fetch of the PAC itself.
690-
// 2. To proxy the generate_204 fetch through a PAC would require a number of things
691-
// happen before the fetch can commence, namely:
692-
// a) the PAC script be fetched
693-
// b) a PAC script resolver service be fired up and resolve the captive portal
694-
// server.
695-
// Network validation could be delayed until these prerequisities are satisifed or
696-
// could simply be left to race them. Neither is an optimal solution.
697-
// 3. PAC scripts are sometimes used to block or restrict Internet access and may in
698-
// fact block fetching of the generate_204 URL which would lead to false negative
699-
// results for network validation.
700-
boolean fetchPac = false;
701-
final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy();
702-
if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
703-
url = new URL(proxyInfo.getPacFileUrl().toString());
704-
fetchPac = true;
706+
URL pacUrl = null, httpUrl = null, httpsUrl = null;
707+
708+
// On networks with a PAC instead of fetching a URL that should result in a 204
709+
// response, we instead simply fetch the PAC script. This is done for a few reasons:
710+
// 1. At present our PAC code does not yet handle multiple PACs on multiple networks
711+
// until something like https://android-review.googlesource.com/#/c/115180/ lands.
712+
// Network.openConnection() will ignore network-specific PACs and instead fetch
713+
// using NO_PROXY. If a PAC is in place, the only fetch we know will succeed with
714+
// NO_PROXY is the fetch of the PAC itself.
715+
// 2. To proxy the generate_204 fetch through a PAC would require a number of things
716+
// happen before the fetch can commence, namely:
717+
// a) the PAC script be fetched
718+
// b) a PAC script resolver service be fired up and resolve the captive portal
719+
// server.
720+
// Network validation could be delayed until these prerequisities are satisifed or
721+
// could simply be left to race them. Neither is an optimal solution.
722+
// 3. PAC scripts are sometimes used to block or restrict Internet access and may in
723+
// fact block fetching of the generate_204 URL which would lead to false negative
724+
// results for network validation.
725+
final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy();
726+
if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
727+
try {
728+
pacUrl = new URL(proxyInfo.getPacFileUrl().toString());
729+
} catch (MalformedURLException e) {
730+
validationLog("Invalid PAC URL: " + proxyInfo.getPacFileUrl().toString());
731+
return CaptivePortalProbeResult.FAILED;
705732
}
706-
final StringBuffer connectInfo = new StringBuffer();
707-
String hostToResolve = null;
708-
// Only resolve a host if HttpURLConnection is about to, to avoid any potentially
709-
// unnecessary resolution.
710-
if (proxyInfo == null || fetchPac) {
711-
hostToResolve = url.getHost();
712-
} else if (proxyInfo != null) {
713-
hostToResolve = proxyInfo.getHost();
733+
}
734+
735+
if (pacUrl == null) {
736+
try {
737+
httpUrl = new URL(getCaptivePortalServerUrl(mContext, false));
738+
httpsUrl = new URL(getCaptivePortalServerUrl(mContext, true));
739+
} catch (MalformedURLException e) {
740+
validationLog("Bad validation URL: " + getCaptivePortalServerUrl(mContext, false));
741+
return CaptivePortalProbeResult.FAILED;
714742
}
715-
if (!TextUtils.isEmpty(hostToResolve)) {
716-
connectInfo.append(", " + hostToResolve + "=");
717-
final InetAddress[] addresses =
718-
mNetworkAgentInfo.network.getAllByName(hostToResolve);
743+
}
744+
745+
long startTime = SystemClock.elapsedRealtime();
746+
747+
// Pre-resolve the captive portal server host so we can log it.
748+
// Only do this if HttpURLConnection is about to, to avoid any potentially
749+
// unnecessary resolution.
750+
String hostToResolve = null;
751+
if (pacUrl != null) {
752+
hostToResolve = pacUrl.getHost();
753+
} else if (proxyInfo != null) {
754+
hostToResolve = proxyInfo.getHost();
755+
} else {
756+
hostToResolve = httpUrl.getHost();
757+
}
758+
759+
if (!TextUtils.isEmpty(hostToResolve)) {
760+
String probeName = ValidationProbeEvent.getProbeName(ValidationProbeEvent.PROBE_DNS);
761+
final Stopwatch dnsTimer = new Stopwatch().start();
762+
try {
763+
InetAddress[] addresses = mNetworkAgentInfo.network.getAllByName(hostToResolve);
764+
long dnsLatency = dnsTimer.stop();
765+
ValidationProbeEvent.logEvent(mNetId, dnsLatency,
766+
ValidationProbeEvent.PROBE_DNS, ValidationProbeEvent.DNS_SUCCESS);
767+
final StringBuffer connectInfo = new StringBuffer(", " + hostToResolve + "=");
719768
for (InetAddress address : addresses) {
720769
connectInfo.append(address.getHostAddress());
721770
if (address != addresses[addresses.length-1]) connectInfo.append(",");
722771
}
772+
validationLog(probeName + " OK " + dnsLatency + "ms" + connectInfo);
773+
} catch (UnknownHostException e) {
774+
long dnsLatency = dnsTimer.stop();
775+
ValidationProbeEvent.logEvent(mNetId, dnsLatency,
776+
ValidationProbeEvent.PROBE_DNS, ValidationProbeEvent.DNS_FAILURE);
777+
validationLog(probeName + " FAIL " + dnsLatency + "ms, " + hostToResolve);
723778
}
724-
validationLog("Checking " + url.toString() + " on " +
725-
mNetworkAgentInfo.networkInfo.getExtraInfo() + connectInfo);
779+
}
780+
781+
CaptivePortalProbeResult result;
782+
if (pacUrl != null) {
783+
result = sendHttpProbe(pacUrl, ValidationProbeEvent.PROBE_PAC);
784+
} else if (mUseHttps) {
785+
result = sendParallelHttpProbes(httpsUrl, httpUrl);
786+
} else {
787+
result = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP);
788+
}
789+
790+
long endTime = SystemClock.elapsedRealtime();
791+
792+
sendNetworkConditionsBroadcast(true /* response received */,
793+
result.isPortal() /* isCaptivePortal */,
794+
startTime, endTime);
795+
796+
return result;
797+
}
798+
799+
/**
800+
* Do a URL fetch on a known server to see if we get the data we expect.
801+
* Returns HTTP response code.
802+
*/
803+
@VisibleForTesting
804+
protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType) {
805+
HttpURLConnection urlConnection = null;
806+
int httpResponseCode = 599;
807+
String redirectUrl = null;
808+
final Stopwatch probeTimer = new Stopwatch().start();
809+
try {
726810
urlConnection = (HttpURLConnection) mNetworkAgentInfo.network.openConnection(url);
727-
urlConnection.setInstanceFollowRedirects(fetchPac);
811+
urlConnection.setInstanceFollowRedirects(probeType == ValidationProbeEvent.PROBE_PAC);
728812
urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
729813
urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
730814
urlConnection.setUseCaches(false);
@@ -738,7 +822,9 @@ protected CaptivePortalProbeResult isCaptivePortal() {
738822
// Time how long it takes to get a response to our request
739823
long responseTimestamp = SystemClock.elapsedRealtime();
740824

741-
validationLog("isCaptivePortal: ret=" + httpResponseCode +
825+
validationLog(ValidationProbeEvent.getProbeName(probeType) + " " + url +
826+
" time=" + (responseTimestamp - requestTimestamp) + "ms" +
827+
" ret=" + httpResponseCode +
742828
" headers=" + urlConnection.getHeaderFields());
743829
// NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive
744830
// portal. The only example of this seen so far was a captive portal. For
@@ -756,14 +842,10 @@ protected CaptivePortalProbeResult isCaptivePortal() {
756842
httpResponseCode = 204;
757843
}
758844

759-
if (httpResponseCode == 200 && fetchPac) {
845+
if (httpResponseCode == 200 && probeType == ValidationProbeEvent.PROBE_PAC) {
760846
validationLog("PAC fetch 200 response interpreted as 204 response.");
761847
httpResponseCode = 204;
762848
}
763-
764-
sendNetworkConditionsBroadcast(true /* response received */,
765-
httpResponseCode != 204 /* isCaptivePortal */,
766-
requestTimestamp, responseTimestamp);
767849
} catch (IOException e) {
768850
validationLog("Probably not a portal: exception " + e);
769851
if (httpResponseCode == 599) {
@@ -774,11 +856,68 @@ protected CaptivePortalProbeResult isCaptivePortal() {
774856
urlConnection.disconnect();
775857
}
776858
}
777-
final int probeType = ValidationProbeEvent.PROBE_HTTP;
778859
ValidationProbeEvent.logEvent(mNetId, probeTimer.stop(), probeType, httpResponseCode);
779860
return new CaptivePortalProbeResult(httpResponseCode, redirectUrl);
780861
}
781862

863+
private CaptivePortalProbeResult sendParallelHttpProbes(URL httpsUrl, URL httpUrl) {
864+
// Number of probes to wait for. We might wait for all of them, but we might also return if
865+
// only one of them has replied. For example, we immediately return if the HTTP probe finds
866+
// a captive portal, even if the HTTPS probe is timing out.
867+
final CountDownLatch latch = new CountDownLatch(2);
868+
869+
// Which probe result we're going to use. This doesn't need to be atomic, but it does need
870+
// to be final because otherwise we can't set it from the ProbeThreads.
871+
final AtomicReference<CaptivePortalProbeResult> finalResult = new AtomicReference<>();
872+
873+
final class ProbeThread extends Thread {
874+
private final boolean mIsHttps;
875+
private volatile CaptivePortalProbeResult mResult;
876+
877+
public ProbeThread(boolean isHttps) {
878+
mIsHttps = isHttps;
879+
}
880+
881+
public CaptivePortalProbeResult getResult() {
882+
return mResult;
883+
}
884+
885+
@Override
886+
public void run() {
887+
if (mIsHttps) {
888+
mResult = sendHttpProbe(httpsUrl, ValidationProbeEvent.PROBE_HTTPS);
889+
} else {
890+
mResult = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP);
891+
}
892+
if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) {
893+
// HTTPS succeeded, or HTTP found a portal. Don't wait for the other probe.
894+
finalResult.compareAndSet(null, mResult);
895+
latch.countDown();
896+
}
897+
// Signal that one probe has completed. If we've already made a decision, or if this
898+
// is the second probe, the latch will be at zero and we'll return a result.
899+
latch.countDown();
900+
}
901+
}
902+
903+
ProbeThread httpsProbe = new ProbeThread(true);
904+
ProbeThread httpProbe = new ProbeThread(false);
905+
httpsProbe.start();
906+
httpProbe.start();
907+
908+
try {
909+
latch.await();
910+
} catch (InterruptedException e) {
911+
validationLog("Error: probe wait interrupted!");
912+
return CaptivePortalProbeResult.FAILED;
913+
}
914+
915+
// If there was no deciding probe, that means that both probes completed. Return HTTPS.
916+
finalResult.compareAndSet(null, httpsProbe.getResult());
917+
918+
return finalResult.get();
919+
}
920+
782921
/**
783922
* @param responseReceived - whether or not we received a valid HTTP response to our request.
784923
* If false, isCaptivePortal and responseTimestampMs are ignored

0 commit comments

Comments
 (0)
This repository has been archived.