1.背景
最近在做一些调试工具的工作,陆陆续续做了一些设备信息、route、帧率、UI调试等功能,目前需要给 QA 的同学添加抓包
和数据监控
的功能。因为Flutter
的网络请求,跟 Native
的还不太一样,不能直接在 Wifi 里面直接开启代理就可以用,所以这里需要特殊处理一下。
2.分析
首先我们先看一下flutter
如何进行过网络请求的。
// 创建一个HttpClient:
HttpClient httpClient = HttpClient();
// 打开Http连接,设置请求头:
HttpClientRequest request = await httpClient.getUrl(uri);
// 这一步可以使用任意Http Method,如httpClient.post(...)、httpClient.delete(...)等。如果包含Query参数,可以在构建uri时添加,如:
Uri uri = Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
"xx":"xx",
"yy":"dd"
});
// 通过HttpClientRequest可以设置请求header,如:
request.headers.add("user-agent", "test");
// 如果是post或put等可以携带请求体方法,可以通过HttpClientRequest对象发送request body,如:
String payload="...";
request.add(utf8.encode(payload));
// 等待连接服务器:
HttpClientResponse response = await request.close();
// 读取响应内容:
String responseBody = await response.transform(utf8.decoder).join();
// 请求结束,关闭HttpClient:
httpClient.close();
Flutter 的所有的网路操作,都是基于HttpClient
来进行的,比如Dio
库最终使用HttpClinet
进行网络请求。源码追踪一下:
// 任意发送一个请求:
var response = await Dio().get('http://www.google.com');
// dio_mixin.dart
@override
Future> get(
String path, {
Map? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) {
return request(
path,
queryParameters: queryParameters,
options: checkOptions('GET', options),
onReceiveProgress: onReceiveProgress,
cancelToken: cancelToken,
);
}
@override
Future> request(
String path, {
data,
Map? queryParameters,
CancelToken? cancelToken,
Options? options,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
..... 一系列判断 + 数据组装
return fetch(requestOptions);
}
@override
Future> fetch(RequestOptions requestOptions) async {
......
// Initiate Http requests
Future> _dispatchRequest(RequestOptions reqOpt) async {
var cancelToken = reqOpt.cancelToken;
ResponseBody responseBody;
try {
var stream = await _transformData(reqOpt);
responseBody = await httpClientAdapter.fetch(
reqOpt,
stream,
cancelToken?.whenCancel,
);
.......
}
// io_daapter.dart
@override
Future fetch(
RequestOptions options,
Stream? requestStream,
Future? cancelFuture,
) async {
if (_closed) {
throw Exception(
"Can't establish connection after [HttpClientAdapter] closed!");
}
var _httpClient = _configHttpClient(cancelFuture, options.connectTimeout);
........
HttpClient _configHttpClient(Future? cancelFuture, int connectionTimeout) {
var _connectionTimeout = connectionTimeout > 0
? Duration(milliseconds: connectionTimeout)
: null;
if (cancelFuture != null) {
var _httpClient = HttpClient();
_httpClient.userAgent = null;
if (onHttpClientCreate != null) {
//user can return a HttpClient instance
_httpClient = onHttpClientCreate!(_httpClient) ?? _httpClient;
}
_httpClient.idleTimeout = Duration(seconds: 0);
cancelFuture.whenComplete(() {
Future.delayed(Duration(seconds: 0)).then((e) {
try {
_httpClient.close(force: true);
} catch (e) {
//...
}
});
});
return _httpClient..connectionTimeout = _connectionTimeout;
}
if (_defaultHttpClient == null) {
_defaultHttpClient = HttpClient();
_defaultHttpClient!.idleTimeout = Duration(seconds: 3);
if (onHttpClientCreate != null) {
//user can return a HttpClient instance
_defaultHttpClient =
onHttpClientCreate!(_defaultHttpClient!) ?? _defaultHttpClient;
}
_defaultHttpClient!.connectionTimeout = _connectionTimeout;
}
return _defaultHttpClient!;
}
我们在看一下 关于 HttpClient
的相关源码.并找出对我们有帮助内容。
factory HttpClient({SecurityContext? context}) {
HttpOverrides? overrides = HttpOverrides.current;
if (overrides == null) {
return _HttpClient(context);
}
return overrides.createHttpClient(context);
}
.....
// 设置 代理 属性
void set findProxy(String Function(Uri url)? f);
.....
// 本地 https 证书校验属性
void set badCertificateCallback(
bool Function(X509Certificate cert, String host, int port)? callback);
在HttpClient
的源码我们发现真正返回的是HttpClient
的对象 _HttpClient
。 同时HttpClient
的创建方式是根据 HttpOverrides
来做区分的,我们在看一下 HttpOverrides
具体是什么:
abstract class HttpOverrides {
static HttpOverrides? _global;
static HttpOverrides? get current {
return Zone.current[_httpOverridesToken] ?? _global;
}
/// The [HttpOverrides] to use in the root [Zone].
///
/// These are the [HttpOverrides] that will be used in the root Zone, and in
/// Zone's that do not set [HttpOverrides] and whose ancestors up to the root
/// Zone do not set [HttpOverrides].
static set global(HttpOverrides? overrides) {
_global = overrides;
}
........
/// Returns a new [HttpClient] using the given [context].
///
/// When this override is installed, this function overrides the behavior of
/// `new HttpClient`.
HttpClient createHttpClient(SecurityContext? context) {
return _HttpClient(context);
}
........
}
同时我们也找到了,关于设置 代理和本地Https
证书校验的开关逻辑。
3.切入点
1. HttpClient
经过上面对相关代码的分析,我们可以得出一个结论,我们直接对 HttpClient
进行操作,对代码是无侵入性的,直接在HttpClient
中设置proxy
、获取请求的相关uri
、header
、request
、response
等内容,实现HttpClient
、HttpClientRequest
、HttpClientResponse
,在这些实现中采集需要的数据。
2. HttpOverride
要实现功能且无侵入原有代码逻辑,HttpOverride
是关键。我们可以通过实现HttpOverride
,然后复写createHttpClient
来创建我们自己的HttpClient
3.proxy
代理功能的实现可以通过设置HttpClient
的findProxy
逻辑来实现,如果项目做了本地Https
的证书校验,则可以通过设置 badCertificateCallback
来对接口进行校验逻辑。
4.监控
实现HttpOverride
,覆写createHttpClient
// 继承 HttpOverrides 实现自己的,同时保存原有的 HttpOverrides
class TitanHttpOverrides extends HttpOverrides {
final HttpOverrides? origin;
TitanHttpOverrides({this.origin});
// 覆写 createHttpClient
// 原有 HttpOverrides存在,直接创建 _httpClient对象,
// HttpOverrides 不存在,置空 HttpOVerrides.global 创建默认 _httpClient; 用自己实现的HttpClient持有。
@override
HttpClient createHttpClient(SecurityContext? context) {
if (origin != null) {
return TitanHttpClient(origin!.createHttpClient(context));
}
HttpOverrides.global = null;
final httpClient = TitanHttpClient(HttpClient(context: context));
HttpOverrides.global = this;
return httpClient;
}
}
2. 设置 HttpOVerrides.globle
// 首先通过 HttpOverrides.current 获取当前的 HttpOverrides,如果之前设置有,不要破坏原始的 HttpOverrides
final HttpOverrides? origin = HttpOverrides.current;
//设置HttpOverrides.global 为自己实现的 HttpOverrides,同时保存原始 HttpOverrides
HttpOverrides.global = TitanHttpOverrides(origin: origin);
这一步可以写在 main.dart
或者自己需要的位置。
3. 实现自己的HttpClient
、HttpClientRequest
、HttpClientResponse
.
// 实现 HttpClinet ,override 所有函数
class HttpClientAdapter implements HttpClient {
final HttpClient origin;
HttpClientAdapter(this.origin);
// override 所有函数,直接return _httpClient的实现。
@override
void addCredentials(Uri url, String realm, HttpClientCredentials credentials) {
origin.addCredentials(url, realm, credentials);
}
......
// 在关键的一些函数中,加入自己要监控的内容。
@override
Future get(String host, int port, String path) {
return monitor(origin.get(host, port, path));
}
}
httpClientRequest
class HttpClientRequestAdapter implements HttpClientRequest {
final HttpClientRequest origin;
HttpClientRequestAdapter(this.origin);
@override
bool get bufferOutput => origin.bufferOutput;
......
}
HttpClientResponse
class HttpClientResponseAdapter implements HttpClientResponse {
final HttpClientResponse origin;
HttpClientResponseAdapter(this.origin);
........
}
代码过多,这里就不一一贴上了,明白思路很重要。
5.抓包
根据上面你的思路,我们在 自己实现的HttpClient
中进行代理的相关设置。
// 不校验 App 的 https 证书
// 如果app 做了 https 的本地证书校验功能,抓包到的接口数据 会显示 unknown,在这里跳过 https 证书校验功能,就能正常显示了。
bool _badCertificateCallback(X509Certificate cert, String host, int port) {
return true;
}
// 设置代理地址
String _proxyString(url) {
return HttpClient.findProxyFromEnvironment(url, environment: {
'http_proxy': titanStore.httpProxyInfo?.httpProxy ?? '',
'https_proxy': titanStore.httpProxyInfo?.httpsProxy ?? '',
'no_proxy': titanStore.httpProxyInfo?.noProxy ?? '',
});
}
// 在 httpClinet 的构造函数中直接设置就可以了。
this.badCertificateCallback = _badCertificateCallback;
this.findProxy = _proxyString;
有时候 设置https
证书关闭之后,抓包之后还会显示unknown
。是因为 网络请求的时候 覆盖了之前的配置,需要在httpClient
中,进行判断操作。
@override
set badCertificateCallback(bool Function(X509Certificate cert, String host, int port)? callback) {
// 防止接口设置本地证书校验,强制关闭。
origin.badCertificateCallback = isOpenProxy ? _badCertificateCallback : callback;
}
这样一个代理功能就实现了。
6.效果展示
在这里,我们请求一个国家气象局的接口
// 在这里,我们直接直接用 dio 发起请求,不对代码做任何修改。
void getHttp() async {
try {
var response = await Dio().get('http://www.weather.com.cn/data/sk/101010100.html');
} catch (e) {}
}
从调试工具的 网络中我们可以看到已经拿到请求的相关数据了。
我们在打开代理模块,配置上本机的ip和端口,开启代理模式。
我们就可以在charles 中看到 这个接口的数据了。
7 总结
经过上面的一系列操作之后,我们目前就可以在无侵入的情况下,拿到网络请求的数据,同时也脱离代理实现的 proxy 功能。