1. 问题描述

使用手机流量,Flutter页面加载一个用户头像时,总是显示不出来,接入公司内部Wi-fi网络可恢复。有问题的app版本简称A版本。使用上一个线上版本app(简称B版本),接入任何网络均可显示该用户头像。

2. 问题分析

阶段一

第一步肯定是想先看看接入公司网络和使用手机流量时,头像图片下载的http请求和响应内容有什么区别。那么就需要抓包,使用charles代理抓包时发现,flutter app的包抓不到。。。首先想到的时换wireShark来抓,由于WireShark直接抓取网卡数据,且WireShark不用设置代理,使用更方便。

我的开发环境是在Mac OS下的,打开Wireshark,连上iphone到电脑,终端中输入如下命令rvictl -s YOUR-PHONES-UDID,虚拟出一个网络接口,如rvi0,然后在wireshark中选择监听它即可。

分别将iphone接入公司Wi-Fi和使用自己的流量时,使用A版本触发图片下载,发现http请求的响应码不同,且ip地址也不同。应该是访问该域名时,server端针对不同的ip入口设置了不同的访问控制。接入公司网络时,由于图片所在的url也属于集团内部的域名,所以直接放行了,响应代码为302,并在响应报文中给出了重定向的url。而使用手机流量时,响应码为403,禁止访问。

android flutter抓包 flutter应用抓包_android flutter抓包

然而,在使用手机流量时,直接通过浏览器访问这个图片地址,却是可以正常打开的。可以看到包含重定向的一次请求,总共发了两次请求,第一次并没有返回403。

android flutter抓包 flutter应用抓包_flutter_02

经过对比请求的header,最终判断出是服务端对user-agent字段有判断,flutter的app使用httpClient发送的http请求,默认的user-agent值为dart的版本号,例如Dart/2.12 (dart:io)。测试一下我的推断,在user-agent中加入了iphone之类的移动端标识后,我们的A版本app也能正常拿到302的响应了。至于具体是怎么加的,以及后续的重定向请求是否有成功,在阶段三中再展开说。

阶段二

下面要分析一下为什么B版本的app在手机流量网络环境下可以正常访问这个图片。再次经过WireShark抓包发现,B版本app访问图片的ip是一个v6地址,而B版本的http header和A版本并无大的区别,只是dart版本号不同而已(A版本升级了Flutter版本到2.0.6,B版本Flutter版本为1.17):Dart/2.12 (dart:io)。看来如果通过v6地址来访问这个站点的数据,管控更松一些。那为什么A版本不通过ipv6的地址去访问,而会选择ipv4的地址去访问呢,重新分析A版本的数据包,发现A版本曾经也是尝试通过ipv6的地址连接服务端,但是最终TCP握手失败。失败的原因肯定是升级Flutter和Dart SDK引起的,具体原因还不清楚,考虑到app不可能在这方面轻易进行版本替换和修改,待后续有时间继续分析。

android flutter抓包 flutter应用抓包_flutter_03

阶段三

由于各种客观原因,目前考虑从app端修改。在发送请求时,给user-agent加一段标识即可:

final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
    request.headers.add(name, value); // 此处可以给request的header增加字段或对原有字段进行扩充
});
final HttpClientResponse? response = await request.close();

原本以为问题解决,后来发现仍然A版本仍然拿不到图片数据。从WireShark中已经可以看到,http请求确实拿到了302的响应,那么只有可能是重定向的请求响应返回了错误。由于重定向的地址是一个https地址,所以在WireShark中看不到具体数据,只能用Charles工具来抓包看看。

使用Charles不能直接抓到flutter应用的包,需要在flutter中进行代理设置,临时加入findProxy处理:

static final HttpClient _sharedHttpClient = HttpClient()
    ..autoUncompress = false
    ..findProxy = (url) {
      return HttpClient.findProxyFromEnvironment(url, environment: {
        "http_proxy": 'http://192.168.43.113:8888',
        "https_proxy": 'http://192.168.43.113:8888',
      });
    };

由于我们需要同时抓取http和https的包,需要分别将两种协议代理都指定到安装有charles的电脑的ip上。启动Charles,发现问题所在了,虽然我们在header的user-agent字段里增加了移动端标识,但是在发送重定向后的请求时,user-agent里新增的标识又不见了。从源码的描述看,重定向的请求在发送时会带上原请求的header内容,看来dart的处理有些缺陷,因为尝试直接在header里加入其他字段,重定向的请求发送时,这些字段并没有丢失。最后只能放弃使用request.headers.add()来修改user-agent的内容,而选择直接修改httpClient的user-agent属性,这个访问控制的问题得到解决:

static final HttpClient _sharedHttpClient = HttpClient()
  ..autoUncompress = false
  ..userAgent = 'Dart/2.12 (dart:io), iPhone';

结束语

通过对该问题的分析,不光修改了bug,还get到了如何使用WireShark抓包,如何使用Charles设置代理抓flutter的http和https加密包。以后再分析需要抓flutter数据包问题,相信效率可以提升很多。