How to check for unrestricted Internet access? (captive portal detection) - android

I need to reliably detect if a device has full internet access, i.e. that the user is not confined to a captive portal (also called walled garden), i.e. a limited subnet which forces users to submit their credentials on a form in order to get full access.
My app is automating the authentication process, and therefore it is important to know that full internet access is not available before starting the logon activity.
The question is not about how to check that the network interface is up and in a connected state. It is about making sure the device has unrestricted internet access as opposed to a sandboxed intranet segment.
All the approaches I have tried so far are failing, because connecting to any well-known host would not throw an exception but return a valid HTTP 200 response code because all requests are routed to the login page.
Here are all the approaches I tried but they all return true instead of false for the reasons explained above:
1:
InetAddress.getByName(host).isReachable(TIMEOUT_IN_MILLISECONDS);
isConnected = true; <exception not thrown>
2:
Socket socket = new Socket();
SocketAddress sockaddr = new InetSocketAddress(InetAddress.getByName(host), 80);
socket.connect(sockaddr, pingTimeout);
isConnected = socket.isConnected();
3:
URL url = new URL(hostUrl));
URLConnection urlConn = url.openConnection();
HttpURLConnection httpConn = (HttpURLConnection) urlConn;
httpConn.setAllowUserInteraction(false);
httpConn.setRequestMethod("GET");
httpConn.connect();
responseCode = httpConn.getResponseCode();
isConnected = responseCode == HttpURLConnection.HTTP_OK;
So, how do I make sure I connected to an actual host instead of the login redirection page? Obviously, I could check the actual response body from the 'ping' host I use but it does not look like a proper solution.

For reference, here is the 'official' method from the Android 4.0.1 AOSP code base:
WifiWatchdogStateMachine.isWalledGardenConnection(). I am including the code below just in case the link breaks in the future.
private static final String mWalledGardenUrl = "http://clients3.google.com/generate_204";
private static final int WALLED_GARDEN_SOCKET_TIMEOUT_MS = 10000;
private boolean isWalledGardenConnection() {
HttpURLConnection urlConnection = null;
try {
URL url = new URL(mWalledGardenUrl); // "http://clients3.google.com/generate_204"
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setInstanceFollowRedirects(false);
urlConnection.setConnectTimeout(WALLED_GARDEN_SOCKET_TIMEOUT_MS);
urlConnection.setReadTimeout(WALLED_GARDEN_SOCKET_TIMEOUT_MS);
urlConnection.setUseCaches(false);
urlConnection.getInputStream();
// We got a valid response, but not from the real google
return urlConnection.getResponseCode() != 204;
} catch (IOException e) {
if (DBG) {
log("Walled garden check - probably not a portal: exception "
+ e);
}
return false;
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}
This approach relies on a specific URL, mWalledGardenUrl = "http://clients3.google.com/generate_204" always returning a 204 response code. This will work even if DNS has been interfered with since in that case a 200 code will be returned instead of the expected 204. I have seen some captive portals spoofing requests to this specific URL in order to prevent the Internet not accessible message on Android devices.
Google has a variation of this theme: fetching http://www.google.com/blank.html will return a 200 code with a zero-length response body. So if you get a non-empty body this would be another way to figure out that you are behind a walled garden.
Apple has its own URLs for detecting captive portals: when network is up IOS and MacOS devices would connect to an URL like http://www.apple.com/library/test/success.html, http://attwifi.apple.com/library/test/success.html, or http://captive.apple.com/hotspot-detect.html which must return an HTTP status code of 200 and a body containing Success.
NOTE:
This approach will not work in areas with regionally restricted Internet access such as China where the whole country is a walled garden, and where some Google/Apple services might be blocked. Some of these might not be blocked: http://www.google.cn/generate_204, http://g.cn/generate_204, http://gstatic.com/generate_204 or http://connectivitycheck.gstatic.com/generate_204 — yet these all belong to google so not guaranteed to work.

Another possible solution might be to connect via HTTPS and inspect the target certificate. Not sure if walled gardens actually serve the login page via HTTPS or just drop the connections. In either case, you should be able to see that your destination is not the one you expected.
Of course, you also have the overhead of TLS and certificate checks. Such is the price of authenticated connections, unfortunately.

I believe preventing redirection for your connection will work.
URL url = new URL(hostUrl));
HttpURLConnection httpConn = (HttpURLConnection)url.openConnection();
/* This line prevents redirects */
httpConn.setInstanceFollowRedirects( false );
httpConn.setAllowUserInteraction( false );
httpConn.setRequestMethod( "GET" );
httpConn.connect();
responseCode = httpConn.getResponseCode();
isConnected = responseCode == HttpURLConnection.HTTP_OK;
If that doesn't work, then I think the only way to do it is to check the body of the response.

This has been implemented on Android 4.2.2+ version - I find their approach fast and interesting :
CaptivePortalTracker.java detects walled garden as follows
- Try to connect to www.google.com/generate_204
- Check that the HTTP response is 204
If the check fails, we are in a walled garden.
private boolean isCaptivePortal(InetAddress server) {
HttpURLConnection urlConnection = null;
if (!mIsCaptivePortalCheckEnabled) return false;
mUrl = "http://" + server.getHostAddress() + "/generate_204";
if (DBG) log("Checking " + mUrl);
try {
URL url = new URL(mUrl);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setInstanceFollowRedirects(false);
urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
urlConnection.setUseCaches(false);
urlConnection.getInputStream();
// we got a valid response, but not from the real google
return urlConnection.getResponseCode() != 204;
} catch (IOException e) {
if (DBG) log("Probably not a portal: exception " + e);
return false;
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}

if you are already using retrofit you can do it by retrofit. just make a ping.html page and send an head request to it using retrofit and make sure your http client is configured like below: (followRedirects(false) part is the most important part)
private OkHttpClient getCheckInternetOkHttpClient() {
return new OkHttpClient.Builder()
.readTimeout(2L, TimeUnit.SECONDS)
.connectTimeout(2L, TimeUnit.SECONDS)
.followRedirects(false)
.build();
}
then build your retrofit like below:
private InternetCheckApi getCheckInternetRetrofitApi() {
return (new Retrofit.Builder())
.baseUrl("[base url of your ping.html page]")
.addConverterFactory(GsonConverterFactory.create(new Gson()))
.client(getCheckInternetOkHttpClient())
.build().create(InternetCheckApi.class);
}
your InternetCheckApi.class would be like:
public interface InternetCheckApi {
#Headers({"Content-Typel: application/json"})
#HEAD("ping.html")
Call<Void> checkInternetConnectivity();
}
then you can use it like below:
getCheckInternetOkHttpClient().checkInternetConnectivity().enqueue(new Callback<Void>() {
public void onResponse(Call<Void> call, Response<Void> response) {
if(response.code() == 200) {
//internet is available
} else {
//internet is not available
}
}
public void onFailure(Call<Void> call, Throwable t) {
//internet is not available
}
}
);
note that your internet check http client must be separate from your main http client.

This is best done here as in AOSP :
https://github.com/aosp-mirror/platform_frameworks_base/blob/6bebb8418ceecf44d2af40033870f3aabacfe36e/core/java/android/net/captiveportal/CaptivePortalProbeResult.java#L61
https://github.com/aosp-mirror/platform_frameworks_base/blob/e3a0f42e8e8678f6d90ddf104d485858fbb2e35b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
private static final String GOOGLE_PING_URL = "http://google.com/generate_204";
private static final int SOCKET_TIMEOUT_MS = 10000;
public boolean isCaptivePortal () {
try {
URL url = new URL(GOOGLE_PING_URL);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
urlConnection.setUseCaches(false);
urlConnection.getInputStream();
return (urlConnection.getResponseCode() != 204)
&& (urlConnection.getResponseCode() >= 200)
&& (urlConnection.getResponseCode() <= 399);
} catch (Exception e) {
// for any exception throw an exception saying check was unsuccesful
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}
Please note this will probably not work on a proxy network and something more advanced as in the AOSP url needs to be done

Related

Unable To Implement Digest Auth In Android Using bare-bones-digest library

I am trying to implement the Digest auth in android. I read many posts and they say HttpUrlConnection class does not support the Digest auth. However, I have implemented it using bare-bones-digest library. Now it is working fine but the API call became very slow. It is taking double time to load the data as compared to it was taking with basic auth. Using base-bones-digest they say to avoid sending each request twice, in subsequent requests the client can reuse the challenge. Only the first request will have to be sent twice. But no implementation is given for that.
HttpURLConnection httpURLConnection = null;
try {
if(mAuthorizationString != null && !mAuthorizationString.equals("")){
URL url = new URL(apiEndpoint);
httpURLConnection = (HttpURLConnection) url.openConnection();
DigestAuthentication auth = DigestAuthentication.fromResponse(httpURLConnection);
// ...with correct credentials
auth.username("username").password("password");
httpURLConnection.setRequestProperty(DigestChallengeResponse.HTTP_HEADER_AUTHORIZATION,
mAuthorizationString);
}
else{
URL url = new URL(apiEndpoint);
httpURLConnection = (HttpURLConnection) url.openConnection();
// Step 2. Make the request and check to see if the response contains an authorization challenge
if (httpURLConnection.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
// Step 3. Create an authentication object from the challenge...
DigestAuthentication auth = DigestAuthentication.fromResponse(httpURLConnection);
// ...with correct credentials
auth.username("username").password("password");
// Step 4 (Optional). Check if the challenge was a digest challenge of a supported type
if (!auth.canRespond()) {
// No digest challenge or a challenge of an unsupported type - do something else or fail
return httpURLConnection;
}
// Step 5. Create a new connection, identical to the original one...
httpURLConnection = (HttpURLConnection) url.openConnection();
mAuthorizationString = auth.getAuthorizationForRequest(requestMethod, httpURLConnection.getURL().getPath());
// ...and set the Authorization header on the request, with the challenge response
httpURLConnection.addRequestProperty(DigestChallengeResponse.HTTP_HEADER_AUTHORIZATION,
mAuthorizationString);
}
}
} catch (IOException e) {
e.printStackTrace();
}
I have fixed it Myself by updating the code as follows.
HttpURLConnection httpURLConnection = null;
try {
URL url = new URL(apiEndpoint);
httpURLConnection = (HttpURLConnection) url.openConnection();
if(sAuthorizationString != null && !sAuthorizationString.equals("")){
httpURLConnection.addRequestProperty(DigestChallengeResponse.HTTP_HEADER_AUTHORIZATION,
sAuthorizationString);
}
else{
// Step 2. Make the request and check to see if the response contains an authorization challenge
if (httpURLConnection.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
// Step 3. Create an authentication object from the challenge...
DigestAuthentication auth = DigestAuthentication.fromResponse(httpURLConnection);
// ...with correct credentials
auth.username("username").password("password");
// Step 4 (Optional). Check if the challenge was a digest challenge of a supported type
if (!auth.canRespond()) {
// No digest challenge or a challenge of an unsupported type - do something else or fail
return httpURLConnection;
}
// Step 5. Create a new connection, identical to the original one...
httpURLConnection = (HttpURLConnection) url.openConnection();
sAuthorizationString = auth.getAuthorizationForRequest(requestMethod, httpURLConnection.getURL().getPath());
// ...and set the Authorization header on the request, with the challenge response
httpURLConnection.addRequestProperty(DigestChallengeResponse.HTTP_HEADER_AUTHORIZATION,
sAuthorizationString);
}
}
} catch (IOException e) {
e.printStackTrace();
}

HttpURLConnection getResponseCode() deos not return if there is no Internet connection

I am using a HttpURLConnection to check whether the server URL is available or not by using the following code:
try {
boolean connectionFailed = false;
URL knownURL = new URL("http://www.google.com");
httpConnection = (HttpURLConnection) knownURL.openConnection();
httpConnection.setConnectTimeout(5000);
responseCode = httpConnection.getResponseCode();
if (responseCode != 200) {
status = ConnectionStatus.NOT_CONNECTED;
}
}
catch(Exception e) {
connctionFailed = true;
}
This code is working fine under normal conditions. But when there is no Internet connection (because either the router is disconnected or not the hotspot), httpConnection.getResponseCode() is not executed (the function does not return). How can I fix this?
httpConnection.setConnectTimeout(5000)
is a timeout for connection.
This is not a timeout for httpConnection.getResponseCode().
If you add httpConnection.setReadTimeout(2000), httpConnection.getResponseCode()should throw an exception when no connection is available.
You may be having a try catch block at higher layer which is catching the sockettimeout exception.

Android check internet connection when 3g is available but not accessible

I've been looking for help on the other questions, but can not find something, while in a review activity need internet connection, even with activated 3G but can not connect (have exceeded data use or firewall) tutorials or help I have found do not include that.
I use this little function to detect Public Wifi redirects to sign on page. If this function returns false, it means you could not connect to the intended page despite having your 3G or Wifi on. With this you can show the user any page, for example a "No Internet Connection" page.
public boolean networkSignOn() {
HttpURLConnection urlConnection = null;
try {
URL url = new URL("http://clients3.google.com/generate_204");
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setInstanceFollowRedirects(false);
urlConnection.setConnectTimeout(10000);
urlConnection.setReadTimeout(10000);
urlConnection.setUseCaches(false);
urlConnection.getInputStream();
return urlConnection.getResponseCode() == 204;
} catch (IOException e) {
Log.v("Walled garden check - probably not a portal: exception " + e, "");
return false;
} finally {
if (urlConnection != null) urlConnection.disconnect();
}
}

HttpURLConnection with POST "times out" on ICS, works in HC

My Android tablet application does not work with ICS due to a Login problem. When I looked at my code and ran it under debug mode on an ICS tablet, I see the problem but I don't understand it. The code functions correctly on all Honeycomb models that i have tested and in fact I have two tablets hooked up to my computer (one Samsung Galaxy Tab running 3.2, and a Motorola Xoom wifi running 4.0.3) and the code fails on ICS and works on HC.
The failure is a Socket Timeout exception. The timeout was 2000ms, but I upped it to 100000ms to test and it had no impact.
Using the browser on the ICS tablet, I can go to the URL and it responds, so it doesn't appear to be network related.
I am running on a background thread using AsyncTask.
Slurp just takes all of the input from the InputStream and using StringBuilder creates a string representation. Its not actually useful in this request but I added it to see what the server was replying with.
I am POSTing to the page the same way a user authenticates using the form, which is why I am using x-www-form-urlencoded.
Again, this code functions perfectly on Honeycomb but fails on ICS.
The code makes a connection but fails when it asks for a response from the server, almost like the server is still waiting for something... anyway, here is the code:
static public String authenticate(String service_url, String username, String password) throws IOException {
if (username == null || password == null)
throw new IOException();
String charset = "UTF-8";
String query = String.format("Email=%s&Password=%s",URLEncoder.encode(username, charset),URLEncoder.encode(password, charset));
byte [] data = query.getBytes(charset);
URL url = new URL(service_url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("Accept-Charset", charset);
connection.setRequestProperty("Content-Length", Integer.toString(data.length));
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setReadTimeout(5000); // 2 second timeout.
try {
connection.connect();
DataOutputStream pw = new DataOutputStream (connection.getOutputStream());
pw.writeBytes(query);
pw.flush();
pw.close();
int code = connection.getResponseCode(); //SOCKET TIMEOUT HERE
if (code == 200 || code == 302)
{
InputStream is = connection.getInputStream();
String value = slurp(is);
List<String> cookies = connection.getHeaderFields().get("Set-Cookie");
if (cookies == null)
throw new IOException();
for (String cookie : cookies) {
if (cookie.startsWith("cpms")) {
cookieTime = new DateTime(); //crazy but the expires time in the cookie is not actually accurate.
return cookie; // this is the only correct path out.
}
}
}
else
Logger.e(StaticUtils.class, "Invalid response code while logging in: " + code);
}
catch (IOException ioe)
{
Logger.e(StaticUtils.class, ioe);
throw ioe; // log it and then throw it back.
} finally {
connection.disconnect();
}
return null;
}

Consuming a web service in an android application in localhost

I am trying to consume a web service that I created locally from an Android application.
My problem is that in my Android app, at a certain point, I have to give an URL with parameters that looks like this : http://localhost:8080/CalculatorApp/CalculatorWSService/add?i=1&j=1
where CalculatorWS is the web service I use, add is the operation in it and i and j are parameters of add operation. For now I am using a sample app Calculator (from NetBeans) for testing and I want to retrieve the correct URL to give to my web service client (Android app) so it can give me back an XML to parse.
I tried to use that URL mentioned above but it doesn't work.
Does anybody know what is the correct URL to put ?
you need to set URL as 10.0.2.2:portNr
portNr = the given port by ASP.NET Development Server
my current service is running on
localhost:3229/Service.svc
so my url is 10.0.2.2:3229
i'd fixed my problem this way
i hope it helps...
Use this URL:
http://10.0.2.2:8080/CalculatorApp/CalculatorWSService/add?i=1&j=1
Since Android emulator run on Virtual Machine therefore we have to use this IP address instead of localhost or 127.0.0.1
If you're using an emulator then read below paragraph taken from: Referring to localhost from the emulated environment
If you need to refer to your host computer's localhost, such as when
you want the emulator client to contact a server running on the same
host, use the alias 10.0.2.2 to refer to the host computer's loopback
interface. From the emulator's perspective, localhost (127.0.0.1)
refers to its own loopback interface.
sharktiger like you says on the comments, i'll paste here some code to help you to figure how to proced, this code try to connect to a web service and parse the InputStream retrieved, just like #Vikas Patidar and #MisterSquonk says, you must configure the url in the android code like them explain. So, i post my code
and example of call to HttpUtils...
public static final String WS_BASE = "http://www.xxxxxx.com/dev/xxx/";
public static final String WS_STANDARD = WS_BASE + "webserviceoperations.php";
public static final String REQUEST_ENCODING = "iso-8859-1";
/**
* Send a request to the servers and retrieve InputStream
*
* #throws AppException
*/
public static Login logToServer(Login loginData) {
Login result = new Login();
try {
// 1. Build XML
byte[] xml = LoginDAO.generateXML(loginData);
// 2. Connect to server and retrieve data
InputStream is = HTTPUtils.readHTTPContents(WS_STANDARD, "POST", xml, REQUEST_ENCODING, null);
// 3. Parse and get Bean
result = LoginDAO.getFromXML(is, loginData);
} catch (Exception e) {
result.setStatus(new ConnectionStatus(GenericDAO.STATUS_ERROR, MessageConstants.MSG_ERROR_CONNECTION_UNKNOWN));
}
return result;
}
and the method readHTTPContents from my class HTTPUtils
/**
* Get the InputStream contents for a specific URL request, with parameters.
* Uses POST. PLEASE NOTE: You should NOT use this method in the main
* thread.
*
* #param url
* is the URL to query
* #param parameters
* is a Vector with instances of String containing the parameters
*/
public static InputStream readHTTPContents(String url, String requestMethod, byte[] bodyData, String bodyEncoding, Map<String, String> parameters)
throws AppException {
HttpURLConnection connection = null;
InputStream is = null;
try {
URL urlObj = new URL(url);
if (urlObj.getProtocol().toLowerCase().equals("https")) {
trustAllHosts();
HttpsURLConnection https = (HttpsURLConnection) urlObj
.openConnection();
https.setHostnameVerifier(new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
connection = https;
} else {
connection = (HttpURLConnection) urlObj.openConnection();
}
// Allow input
connection.setDoInput(true);
// If there's data, prepare to send.
if (bodyData != null) {
connection.setDoOutput(true);
}
// Write additional parameters if any
if (parameters != null) {
Iterator<String> i = parameters.keySet().iterator();
while (i.hasNext()) {
String key = i.next();
connection.addRequestProperty(key, parameters.get(key));
}
}
// Sets request method
connection.setRequestMethod(requestMethod);
// Establish connection
connection.connect();
// Send data if any
if (bodyData != null) {
OutputStream os = connection.getOutputStream();
os.write(bodyData);
}
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new AppException("Error HTTP code " + connection.getResponseCode());
}
is = connection.getInputStream();
int numBytes = is.available();
if (numBytes <= 0) {
closeInputStream(is);
connection.disconnect();
throw new AppException(MessageConstants.MSG_ERROR_CONNECTION_UNKNOWN);
}
ByteArrayOutputStream content = new ByteArrayOutputStream();
// Read response into a buffered stream
int readBytes = 0;
while ((readBytes = is.read(sBuffer)) != -1) {
content.write(sBuffer, 0, readBytes);
}
ByteArrayInputStream byteStream = new ByteArrayInputStream(content.toByteArray());
content.flush();
return byteStream;
} catch (Exception e) {
// Logger.logDebug(e.getMessage());
throw new AppException(e.getMessage());
} finally {
closeInputStream(is);
closeHttpConnection(connection);
}
}
Hope this help you...

Categories

Resources