【问题标题】:How to check for unrestricted Internet access? (captive portal detection)如何检查不受限制的 Internet 访问? (强制门户检测)
【发布时间】:2022-05-11 21:53:22
【问题描述】:

我需要可靠地检测设备是否具有完整的互联网访问权限,即用户不限于强制门户(也称为围墙花园),即有限的子网,它强制用户在表单上提交他们的凭据以获得完全访问权限。

我的应用程序正在自动执行身份验证过程,因此在开始登录活动之前了解无法完全访问互联网非常重要。

问题不是关于如何检查网络接口是否已启动并处于连接状态。这是为了确保设备具有不受限制的互联网访问权限,而不是沙盒内网段。

到目前为止我尝试过的所有方法都失败了,因为连接到任何知名主机都不会引发异常,而是返回有效的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”主机检查实际的响应正文,但它看起来不是一个合适的解决方案。

【问题讨论】:

  • 由于上游设备(即captive portal)可以将任何内容与 HTTP 200 一起发送回去,实际上检查 HTTP 响应正文似乎是 100% 保证您到达“外面的世界”。当然,即使在那里页面也可以被缓存......但这不太可能。解决缓存问题的常见方法是在请求的 URL 中包含虚假的 HTTP GET 参数(即?time=1234)。

标签: android http authentication redirect connectivity


【解决方案1】:

作为参考,这里是来自 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 设备上出现 Internet 无法访问 消息。

Google 有这个主题的变体:获取 http://www.google.com/blank.html 将返回一个带有零长度响应正文的 200 代码。所以如果你得到一个非空的身体,这将是另一种判断你在围墙花园后面的方法。

Apple 有自己的 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,它必须返回一个 HTTP 状态代码 200和一个包含Success 的正文。

注意: 这种方法不适用于互联网访问受到区域限制的地区,例如中国,整个国家都是围墙的花园,并且某些 Google/Apple 服务可能会被阻止。其中一些可能不会被阻止:http://www.google.cn/generate_204http://g.cn/generate_204http://gstatic.com/generate_204http://connectivitycheck.gstatic.com/generate_204 — 但这些都属于 google,因此不能保证正常工作。

【讨论】:

  • 我想知道 urlConnection.getInputStream() 是否会产生不必要的网络流量。我们可以改用 HttpResponse.getStatusLine().getStatusCode() 吗?
  • @Christian: 需要urlConnection.getInputStream() 才能实际建立连接。它不会产生流量,因为流没有被消耗。在任何情况下,如果您确实使用了流,您会注意到对于此特定 URL,响应正文大小将为零长度。
  • 对不起,掉线了。如果热点中根本没有互联网访问权限,有人知道如何模拟 Android 的强制门户检测吗?我正在使用 Mikrotik 路由器并创建了带有 DNS 记录的热点,例如 .* = ROUTER_IP,因此所有域都被重定向到路由器。它现在在 Windows 和 iOS 中弹出登录页面,但在 Android 中不弹出。我认为 Android 需要一些特殊的东西,如何在没有互联网的情况下显示此通知?
  • 谢谢。正在使用预付费测试并且没有数据计划,当我跳下 Wifi 或移动时,我会看到这种行为,我现在可以检测到。
  • 我想知道如果 android 已经内置了这个检查是否需要。根据我的经验,我可以让 BroadcastReceiver 监听 Co​​nnectivityManager.CONNECTIVITY_ACTION 动作会在检查后给我关于连接的指示强制门户
【解决方案2】:

另一种可能的解决方案是通过 HTTPS 连接并检查目标证书。不确定围墙花园是否真的通过 HTTPS 为登录页面提供服务,或者只是断开连接。无论哪种情况,您都应该能够看到您的目的地不是您所期望的。

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

【讨论】:

    【解决方案3】:

    我相信阻止您的连接重定向会起作用。

    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;
    

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

    【讨论】:

    • 假设上游设备将使用 HTTP 重定向您。理论上,该设备可以通过其他方式路由您,例如 DNS 或基于 IP。它也可以只用标准页面响应任何 HTTP 请求,而根本不需要重定向你。不过,在这种情况下 HTTPS 应该会失败……也许这也是一种可能性?
    • 正如hall 所说,如果它以另一种方式重定向请求,则此方法将失败。我经常处理这种类型的网关,因为我经常住在酒店,虽然我从来没有深入研究过它是如何处理路由的,但它似乎通常是用 HTTP 完成的。
    【解决方案4】:

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

    【讨论】:

    • 这只是原始答案的副本。它来自 v4.2.2 的事实绝对没有改变,因为代码是相同的,并且不会在原始答案中添加任何新内容。
    • 我想知道为什么 Google 将代码从 URL=...clients3.google.com... 更改为 IP 地址!?
    • 可能发现了他们使用 IP 地址的原因:似乎可以配置 generate_204 资源的主机。例如,请参阅android.stackexchange.com/a/105611/65578
    • @hgoebl:只能配置域;域必须有一个/generate_204 路径,返回一个204 状态码。路径似乎不可配置。
    【解决方案5】:

    如果您已经在使用retrofit,您可以通过retrofit 进行操作。只需创建一个 ping.html 页面并使用改造向其发送头部请求,并确保您的 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
         }
      }
    );
    

    请注意,您的 Internet 检查 http 客户端必须与您的主要 http 客户端分开。

    【讨论】:

      【解决方案6】:

      这最好在 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 url

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-05-27
        • 1970-01-01
        • 2016-01-02
        • 1970-01-01
        • 2017-09-08
        • 2012-05-06
        相关资源
        最近更新 更多