如何检查是否存在无限制的互联网访问?(强制门户检测)

37

我需要可靠地检测设备是否具有完整的互联网访问权限,即用户没有受限于强制门户(也称为围墙花园),即一个限制性子网,迫使用户在表单中提交其凭据以获得完全访问权限。

我的应用程序正在自动化身份验证过程,因此在开始登录活动之前知道完整的互联网访问权限是否可用非常重要。

问题不是如何检查网络接口是否已启动并处于连接状态。而是要确保设备具有无限制的互联网访问权限,而不是沙盒化的局域网段。

到目前为止,我尝试的所有方法都失败了,因为连接到任何众所周知的主机都不会抛出异常,而是返回有效的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();
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”主机的实际响应正文,但这似乎不是一个适当的解决方案。


4
由于上游设备(即captive portal)可以将任何内容与HTTP 200一起发送回来,实际检查HTTP响应主体似乎是100%保证您正在达到“外部世界”的唯一可能方法。当然,即使如此,页面也可能被缓存...但这种情况不太可能发生。解决缓存问题的常见方法是在请求的URL中包括一个虚假的HTTP GET参数(即?time=1234)。 - hall.stephenk
6个回答

47

以下是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",该URL始终返回204响应代码。即使DNS遭到干扰,这也将起作用,因为在这种情况下,将返回200代码而不是预期的204代码。我看到一些被控制的门户对这个特定的URL进行欺骗请求,以防止Android设备上出现“无法访问互联网”的消息。

谷歌有一个类似的主题:获取http://www.google.com/blank.html将返回一个带有零长度响应正文的200代码。因此,如果您获得了一个非空正文,这将是另一种确定您是否在封闭花园中的方法。

苹果有自己的URL来检测被控制门户:当网络连接正常时,IOS和MacOS设备将连接到像http://www.apple.com/library/test/success.htmlhttp://attwifi.apple.com/library/test/success.htmlhttp://captive.apple.com/hotspot-detect.html这样的URL,该URL必须返回一个HTTP状态代码200和一个包含Success的正文。

注意: 这种方法在区域性限制互联网访问的地区(如整个国家都是“封闭花园”的中国),以及某些可能被阻止的Google/Apple服务中不起作用。其中一些可能不会被阻止:http://www.google.cn/generate_204http://g.cn/generate_204http://gstatic.com/generate_204http://connectivitycheck.gstatic.com/generate_204——但这些都属于谷歌,因此不能保证有效。


我在想urlConnection.getInputStream()是否会产生不必要的网络流量。我们能否使用HttpResponse.getStatusLine().getStatusCode()呢? - Christian
@Christian: 需要使用urlConnection.getInputStream()才能建立连接。它不会生成流量,因为该流未被消耗。无论如何,如果您确实消耗了该流,您将会注意到对于此特定的URL,响应正文的大小将是零长度。 - ccpizza
1
抱歉打扰了。 有人知道如何在Android中模拟捕获门户检测吗?如果热点没有互联网访问权限,我正在使用Mikrotik路由器并创建了一个带有DNS记录的热点,例如.* = ROUTER_IP,因此所有域都被重定向到路由器。它现在在Windows和iOS中弹出登录页面,但在Android中没有。我认为Android需要一些特殊的东西,我该如何使此通知在没有互联网的情况下出现? - Taras
谢谢。我正在使用预付费并没有数据计划,在我离开WiFi或者处于移动状态时,我会看到这种行为,现在我可以检测到它了。 - sobelito
我不知道是否需要此检查,如果Android已经内置了此功能。根据我的经验,我可以使用BroadcastReceiver监听ConnectivityManager.CONNECTIVITY_ACTION操作,它将在检查完捕获式门户后向我提供有关连接状态的指示。 - Gavriel

5

另一个可能的解决方案是通过HTTPS连接并检查目标证书。不确定封闭式花园是否实际通过HTTPS提供登录页面或仅丢弃连接。无论哪种情况,您都应该能够看到您的目标不是您预期的那个。

当然,您还需要TLS和证书检查的开销。不幸的是,这就是经过身份验证的连接的代价。


1

我相信防止连接重定向会起作用。

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;

如果那不起作用,那么我认为唯一的方法就是检查响应正文。


2
这假设上游设备将使用HTTP重定向您。理论上,设备可以通过其他方式路由您,例如DNS或基于IP的路由。它也可以仅对任何HTTP请求响应标准页面,而根本不重定向您。但在这种情况下,HTTPS应该会失败...也许这也是一种可能性? - hall.stephenk
正如Hall所说,如果它以另一种方式重定向请求,那么这种方法将失败。虽然我经常在酒店住宿,但我经常处理这种类型的网关,尽管我从未深入研究过它如何处理路由,但它似乎通常是通过HTTP完成的。 - Khantahr

0

这已经在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();
        }
    }
}

3
这只是原回答的复制品。它来自v4.2.2并不会有任何改变,因为代码相同,并未对原回答内容做出任何新的补充。 - ccpizza
我想知道为什么谷歌将代码从URL=...clients3.google.com...更改为IP地址! - hgoebl
可能已经找到为什么他们使用IP地址的原因:似乎可以配置“generate_204”资源的主机。例如,请参见http://android.stackexchange.com/a/105611/65578。 - hgoebl
@hgoebl:只有域名可以配置;该域名必须具有返回204状态代码的/generate_204路径。该路径似乎无法配置。 - ccpizza

0

最好在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();
            }
        }
}

请注意,这可能无法在代理网络上工作,需要更高级的东西,例如必须对AOSP网址进行更改。

0

如果您已经在使用retrofit,您可以通过retrofit来实现。只需创建一个ping.html页面,并使用retrofit发送一个head请求,确保您的HTTP客户端配置如下:(followRedirects(false)部分是最重要的部分)

private OkHttpClient getCheckInternetOkHttpClient() {
    return new OkHttpClient.Builder()
            .readTimeout(2L, TimeUnit.SECONDS)
            .connectTimeout(2L, TimeUnit.SECONDS)
            .followRedirects(false)
            .build();
}

然后像下面这样构建你的Retrofit:

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客户端分开。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接