Как проверить наличие неограниченного доступа в интернет? (обнаружение портала в неволе)
Мне нужно надежно определить, имеет ли устройство полный доступ к Интернету, т. Е. Что пользователь не ограничен ограниченным порталом (также называемым огороженным садом), то есть ограниченной подсетью, которая вынуждает пользователей отправлять свои учетные данные в форме, чтобы получить полный доступ доступ.
Мое приложение автоматизирует процесс аутентификации, и поэтому важно знать, что полный доступ к Интернету недоступен перед началом входа в систему.
Вопрос не в том, как проверить, что сетевой интерфейс включен и находится в подключенном состоянии. Речь идет о том, чтобы убедиться, что устройство имеет неограниченный доступ в Интернет, а не сегмент изолированной интрасети.
Все подходы, которые я пробовал до сих пор, терпят неудачу, потому что подключение к любому известному хосту не вызовет исключения, а вернет действительный HTTP 200
Код ответа, потому что все запросы направляются на страницу входа.
Вот все подходы, которые я пробовал, но все они возвращаются true
вместо false
по причинам, изложенным выше:
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;
Итак, как мне убедиться, что я подключился к реальному хосту вместо страницы перенаправления входа? Очевидно, что я мог бы проверить фактическое тело ответа от хоста "ping", который я использую, но это не похоже на правильное решение.
5 ответов
Для справки, вот "официальный" метод из базы кода Android 4.0.1 AOSP: WifiWatchdogStateMachine.isWalledGardenConnection (). Я включил приведенный ниже код на случай, если в будущем связь прекратится.
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();
}
}
}
Этот подход опирается на конкретный URL, mWalledGardenUrl = "http://clients3.google.com/generate_204"
всегда возвращаю 204
код ответа. Это будет работать, даже если DNS вмешивался, так как в этом случае 200
код будет возвращен вместо ожидаемого 204
, Я видел некоторые порталы-порталы, подделывающие запросы на этот конкретный URL-адрес, чтобы предотвратить недоступность Интернета сообщения на устройствах Android.
У Google есть вариант этой темы: выборка http://www.google.com/blank.html
вернет 200
код с телом ответа нулевой длины. Так что, если вы получите непустое тело, это будет еще один способ выяснить, что вы находитесь за огороженным садом.
У Apple есть свои собственные URL-адреса для обнаружения захваченных порталов: когда сеть работает, IOS и устройства MacOS подключаются к URL-адресу, например http://www.apple.com/library/test/success.html, http://attwifi.apple.com/library/test/success.html или http://captive.apple.com/hotspot-detect.html который должен возвращать код состояния HTTP 200
и тело, содержащее Success
,
ПРИМЕЧАНИЕ. Этот подход не будет работать в областях с ограниченным доступом в Интернет, таких как Китай, где вся страна представляет собой огороженный сад и где большинство служб Google/Apple заблокированы или отфильтрованы. Некоторые из них не могут быть заблокированы:
http://www.google.cn/generate_204
,http://g.cn/generate_204
,http://gstatic.com/generate_204
или жеhttp://connectivitycheck.gstatic.com/generate_204
- Тем не менее, все они принадлежат Google, поэтому не гарантированно работать.
Другим возможным решением может быть подключение через HTTPS и проверка целевого сертификата. Не уверен, что огороженные сады действительно обслуживают страницу входа через HTTPS или просто сбрасывают соединения. В любом случае вы должны увидеть, что пункт назначения не тот, который вы ожидали.
Конечно, у вас также есть накладные расходы на проверку TLS и сертификатов. Такова цена аутентифицированных соединений, к сожалению.
Я считаю, что предотвращение перенаправления для вашего соединения будет работать.
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;
Если это не сработает, то я думаю, что единственный способ сделать это - проверить тело ответа.
Это было реализовано на Android 4.2.2+ версии - я считаю их подход быстрым и интересным:
CaptivePortalTracker.java обнаруживает огороженный сад следующим образом - попробуйте подключиться к www.google.com/generate_204 - убедитесь, что ответ HTTP равен 204
Если проверка не удалась, мы в огороженном саду.
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();
}
}
}
Если вы уже используете retrofit
Вы можете сделать это путем retrofit
, просто создайте страницу ping.html и отправьте на нее запрос head, используя модификацию, и убедитесь, что ваш http-клиент настроен, как показано ниже: (followRedirects(false)
часть самая важная часть)
private OkHttpClient getCheckInternetOkHttpClient() {
return new OkHttpClient.Builder()
.readTimeout(2L, TimeUnit.SECONDS)
.connectTimeout(2L, TimeUnit.SECONDS)
.followRedirects(false)
.build();
}
затем создайте свою модификацию, как показано ниже:
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);
}
ваш InternetCheckApi.class будет выглядеть так:
public interface InternetCheckApi {
@Headers({"Content-Typel: application/json"})
@HEAD("ping.html")
Call<Void> checkInternetConnectivity();
}
тогда вы можете использовать его, как показано ниже:
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
}
}
);
обратите внимание, что ваш http-клиент проверки интернета должен быть отделен от вашего основного http-клиента.
Это лучше всего сделать здесь, как в AOSP: https://github.com/aosp-mirror/platform_frameworks_base/blob/6bebb8418ceecf44d2af40033870f3aabacfe36e/core/java/android/net/captiveportal/CaptivePortalProbeResult.java#L61
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();
}
}
}
Обратите внимание, что это, вероятно, не будет работать в прокси-сети и что-то более сложное, как в URL-адресе AOSP, необходимо сделать