iOS截取http/https流量
0x01.Why?
做移动测试的同学经常会在app和server中间架设一个代理(例如charles或者fiddler等),由经代理,app和server之间的交互及交互内容变得可视化,使得我们不再摸黑测试。事实上,能够很好的掌握app和server端的交互不仅对于测试,对于开发,对于产品的整个质量提高都是有非常大益处的。但是,有些场景下,架设代理变得不易,或者难于满足要求,举几个例子:
- 想要找出正常用户使用时候,哪些场景最耗流量(你不能让用户挂代理,如果有针对网络流量的优化,挂代理也看不出问题来)。
- 想要找出请求的各种接口中,哪些服务不稳定,如间歇出现4xx或者5xx错误,这需要统计大量的数据,单一客户端挂代理是做不到的(当然服务端监控如果做得好也能实现)。
- 想要找出某些特定条件下(如弱网,网络切换等)客户端自己产生的请求错误或者超时等等。
- 想要查看一些特殊场景下接口是否会发生重复调用,错误调用序列。这些issue往往藏的很深,不易出现。这时候往往需要分析日志的pattern来把问题揪出来,这时候你就会发现,代理软件做日志分析很麻烦,也要导出来专门分析,而且总挂着代理极为不方便(至少不能切换网络,日志也要根据app做筛查,因为一般都是全流量截取)。
这时候需求就变成了:最好在app内部能够截取所有的HTTP/HTTPS流量,以某种方式保存下来,并且能够以某种方式传递给需要用这些数据的人。这其实是一种APM(Application Performance Monitoring)的概念,国外最早已经有人实现了这种功能,如 newrelic https://newrelic.com/ 国内也有一些类似的厂商了。
0x02. How?
先想一下我们每天都在使用的代理工具是如何实现的呢?代理工具会拦截所有的http的请求,记录下我们需要的信息后替代客户端重新发送相同的请求给服务端;拦截返回,记录下想要的东西后返回给客户端。如果JAVA写的多,你可能看到过各种 interceptor 来截取流量。OKHttp的作者介绍这款被广泛应用的http client的时候曾经说过:OKHttp只不过是请求和响应之间做了一堆interceptor而已。
具体落到iOS上。iOS的Foundation框架提供了 URL Loading System 这个库(后面简写为ULS),所有基于URL(例如http://,https:// ,ftp://这些应用层的传输协议)的协议都可以通过ULS提供的基础类和协议来实现,你甚至可以自定义自己的私有应用层通讯协议。
而ULS库里提供了一个强有力的武器 NSURLProtocol。 继承NSURLProtocol 的子类都可以实现截取行为,具体的方式就是:如果注册了某个NSURLProtocol子类,ULS管理的流量都会先交由这个子类处理,这相当于实现了一个拦截器。由于现在处于统治地位的的http client库 AFNetworking和 Alamofire 都是基于 URL Loading System实现的,所以他们俩和使用基础URL Loading System API产生的流量理论上都可以被截取到。
注意一点,NSURLProtocol是一个抽象类,而不是一个协议(protocol)。
为了达到监控流量的目的,我们就先设计一个类来实现NSURLProtocol吧:
// MyHttpProtocol.h
#import <Foundation/Foundation.h>
@interface MyHttpProtocol : NSURLProtocol
@end
//MyHttpProtocol.m
#import <Foundation/Foundation.h>
#import "MyHttpProtocol.h"
@implementation MyHttpProtocol
+(BOOL)canInitWithRequest:(NSURLRequest *)request{
NSString *scheme =[[request URL] scheme];
if([[scheme lowercaseString] isEqualToString:@"http"]||
[[scheme lowercaseString] isEqualToString:@"https"])
{
if([NSURLProtocol propertyForKey:@"processed" inRequest:request]){
return NO;
}
return YES;
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
NSMutableURLRequest * duplicatedRequest;
duplicatedRequest = [request mutableCopy];
[NSURLProtocol setProperty:@YES forKey:@"processed" inRequest:duplicatedRequest];
NSLog(@"%@",request.HTTPBody);
return (NSURLRequest *) duplicatedRequest;
}
上边的MyHttpProtocol类继承了NSURLProtocol,并实现了 NSURLProtocol的两个方法。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
这个方法返回YES,MyHttpProtocol类就会处理一个 request,否则就按照原有方式处理。在上边的代码里,我先判断了协议的类型是不是http/https,如果不是,则返回NO,如果是,则会做一个判断:这个request是否带有一个叫做 "processed"的标签,如果是,则返回NO,不交给MyHttpProtocol处理;如果不是,则交给MyHttpProtocol处理。
重点说一下标签“processed”:每当需要加载一个URL资源时,URL Loading System会询问MyURLProtocol是否处理,如果返回YES,URL Loading System会创建一个MyURLProtocol实例,实例做完拦截工作后,会重新调用原有的方法,如session GET,URL Loading System会再一次被调用,如果在+canInitWithRequest:中总是返回YES,这样URL Loading System又会创建一个MyURLProtocol实例。。。。这样就导致了无限循环。为了避免这种问题,我们可以利用+setProperty:forKey:inRequest:来给被处理过的请求打标签,然后在+canInitWithRequest:中查询该request是否已经处理过了,如果是则返回NO。 上文中的“processed”就是打的一个标签,标签是一个字符串,可以任意取名。而这个打标签的方法,通常会在
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
中实现。
实现这个子类以后,在程序加载的地方,注册这个类,这样,理论上,请注意“理论上”这三个字,就可以截获所有的http/https流量了。注册的代码如下
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:[MyHttpProtocol class]];
return YES;
}
做完了上述工作,我们仍然无法实现我们所想:记录下所有的请求和响应。这是因为:如果你拦截了请求,你就需要对你的拦截负责:比如重新发送拦截的请求,处理请求对应的返回等。这里就需要完成非常多的dirty work了。下面的玩具代码只会处理最简单的情况,如果真实使用,得处理很多细节问题。
为了便于理解,先介绍NSURLProtocol的几个内置的属性,包括:client,request,cachedResponse,类型如下
@property(readonly, retain) id<NSURLProtocolClient> client;
@property(readonly, copy) NSURLRequest *request;
@property(readonly, copy) NSCachedURLResponse *cachedResponse;
这三个概念稍微有点儿绕,先简要说一下:request被用作接收ULS转给NSURLProtocol的请求;client的实现了NSURLProtocolClient这个协议,这里边有一堆callback函数,我们一会儿会用到didLoadData;cachesResponse,顾名思义,请求对应的相应会被缓存在这里。
我们还要实现NSURLProtocol的两个方法。startLoading和stopLoading
- (void)startLoading{
NSLog(@"Start loading -------");
NSLog(@"request url is: %@",self.request.URL); //这里的self.request就是ULS传过来的请求体,这里我们记录下一些请求体的信息。
NSLog(@"http method is:%@",self.request.HTTPMethod); //
for (NSString *key in[self.request.allHTTPHeaderFields allKeys]){ //打印http请求的header
NSLog(@"key:%@,value:%@",key,[self.request.allHTTPHeaderFields objectForKey:key]);
}
//重新转发请求
NSMutableURLRequest *newRequest = [self.request mutableCopy];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
self.task = [session dataTaskWithRequest:newRequest];
[self.task resume];
}
-(void) stopLoading{
NSLog(@"Stop loading -------");
[self.task cancel];
}
通过上述代码,我们成功的记录下来了请求体的一些信息,但是如何记录返回信息呢?由于ULS是异步框架,所以,响应会推给回调函数,我们必须在回调函数里进行截取。为了实现这一功能,我们需要实现 NSURLSessionDataDelegate 这个委托协议(NSURLSessionDataDelegate也有局限性,这里不展开说了)。
@interface MyHttpProtocol ()<NSURLSessionDataDelegate>
@property (nonatomic, strong) NSMutableData *data;
@property (nonatomic, strong) NSURLSessionDataTask *task;
@end
//当服务端返回信息时,这个回调函数会被ULS调用,在这里实现http返回信息的截取
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data]; //返回给URL Loading System接收到的数据,这个很重要,不然光截取不返回,就瞎了。
NSLog(@"--data received");
//下面的代码只打印json类型的http返回。
NSError *error = nil;
NSString *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if(error){
NSLog(@"error occured!");
return;
}
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonString = [[NSString alloc]initWithData:jsonData encoding:NSUTF8StringEncoding];
NSLog(@"nsdata is %@",jsonString);
}
好了,上边这一坨代码,理论上实现了我们想要的功能的最小集:拦截http/https请求和响应,并打印出来。为什么说理论上呢。如果你使用AFNETworking,你会发现,你的代码根本没有被调用。这是因为它根本不屌上边的注册,也就是下边这句代码:
[NSURLProtocol registerClass:[MyHttpProtocol class]];
实际上 ULS允许加载多个NSURLProtocol,它们被存在一个数组里,默认情况下,AFNETWorking只会使用数组里的第一个protocol。这看起来是个悲剧,如果不改源码,我想做的事儿不就止步于此了么?多亏Objective C是动态语言。我们可以用一项“尖端科技”,也就是object-c的动态方法替换来实现动态的修改源码来达到目的。
实现一个类:MySessionConfiguration.m (这部分代码基本照抄的一个叫做Netfox的开源项目,大家有兴趣可以搜索)。
#import <Foundation/Foundation.h>
#import "MySessionConfiguration.h"
#import "MyHttpProtocol.h"
#import <objc/runtime.h>
@implementation MySessionConfiguration
//返回一个默认配置的单体
+ (MySessionConfiguration *) defaultConfiguration{
static MySessionConfiguration *staticConfiguration;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
staticConfiguration =[[MySessionConfiguration alloc] init];
});
return staticConfiguration;
}
- (instancetype) init{
self = [super init];
if(self){
self.isSwizzle=NO;
}
return self;
}
//load被调用的时候,其实吧session.configuration.protocolClasses 这个数组从原有配置换成了只有MyHttpProtocol
- (void)load{
NSLog(@"----configuration load --");
self.isSwizzle=YES;
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?:NSClassFromString(@"NSURLSessionConfiguration");
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
- (void)unload {
self.isSwizzle=NO;
Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?:NSClassFromString(@"NSURLSessionConfiguration");
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
- (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub{
Method originalMethod = class_getInstanceMethod(original, selector);
Method stubMethod = class_getInstanceMethod(stub, selector);
if(!originalMethod || !stubMethod){
[NSException raise:NSInternalInconsistencyException format:@"Could't load NSURLSessionConfiguration "];
}
//真正的替换在这里
method_exchangeImplementations(originalMethod, stubMethod);
}
//返回MyHttpProtocol
- (NSArray *)protocolClasses{
return @[[MyHttpProtocol class]];
}
@end
最后,简单粗暴的,在程序启动的时候加入这么一句:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//就是这一句
[[[MySessionConfiguration alloc] init] load];
return YES;
}
这样,一个简单的监控功能就实现了。实际上,想让它能够变得实用起来还有无数的坑要填,代码量大概再增加20倍吧,这些坑包括:https的证书校验,NSURLConnection和NSURLSession兼容,重定向,超时处理,返回值内容解析,各种异常处理(不能因为你崩了让程序跟着崩了),开关,截获的信息本地存储策略,回传服务端策略等。真正写一个可用的工具不是那么简单。所以,如果金钱允许,还是让公司去采购吧。。。
0x03 BTW:
1.本人OC菜鸟,肯定有理解不当的地方,有高手请多加指正。
2.有小伙伴想一起做的话可以一同起个开源啊,一起利用一下碎片化的时间(除非专职的开发测试,否则几乎没有大把时间和机会写产品形态的测试工具的)。
iOS截取http/https流量的更多相关文章
- Wireshark解密HTTPS流量的两种方法
原理 我们先回顾一下SSL/TLS的整个握手过程: Clienthello:发送客户端的功能和首选项给服务器,在连接建立后,当希望重协商.或者响应服务器的重协商请求时会发送. version:客户端支 ...
- ios 中使用https的知识
先看文章,这篇文章说的是使用AFNetworing进行https时的事项,十分好!http://blog.cnbang.net/tech/2416/ ios中使用https,主要就是使用NSURLCr ...
- wireshark解密本地https流量笔记
此方式支持firefox,chrome 建立path变量 SSLKEYLOGFILE=c:\ssl.key 重启firefox chrome,访问https网站会自动生成ssl session key ...
- IOS 采用https 协议访问接口
申请好证书后,发现ios 仍无法使用https协议访问到数据,发现ios 需要ssl 支持 TLS1.2 . 更改nginx 配置: ssl_protocols TLSv1 TLSv1. TLSv1. ...
- 在服务器上用Fiddler抓取HTTPS流量
转自:http://yoursunny.com/t/2011/FiddlerHTTPS/在服务器上用Fiddler抓取HTTPS流量 阳光男孩 发表于2011-03-19 开发互联网应用的过程中,常常 ...
- FiddlerCoreAPI开发(二)截获HTTPS流量
上一篇文章简单简单分析了fiddlercore自带样例的代码,本篇文章进入主题,介绍如何使用fiddlercore截获HTTPS流量. 当时学习完样例代码后,我觉得结合注释来抓HTTPS的包应该也很简 ...
- 微信H5页面嵌入百度地图---解决手机的webKit定位,ios系统对非https网站不提供支持问题
<script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=yGQt ...
- iOS app 支持HTTPS iOS开发者相关
2016年12月21日更新开发者中心链接https://developer.apple.com/news/?id=12212016b该链接是苹果昨天刚在官网给的正式回复 如下: App Transpo ...
- Burp Suite 抓取http、https流量配置+CA证书安装
HTTPS协议是为了数据传输安全的需要,在HTTP原有的基础上,加入了安全套接字层SSL协议,通过CA证书来验证服务器的身份,并对通信消息进行加密.基于HTTPS协议这些特性,我们在使用Burp Pr ...
随机推荐
- JavaScript处理json格式数据
JSON即JavaScript对象标记,是一种轻量级的数据交换格式,非常适用于服务器与JavaScript的交互.JSON是基于纯文本的数据格式. JSON是JavaScript的原生格式,可以使用J ...
- 剖析Asp.Net Web API路由系统---WebHost部署方式
上一篇我们剖析了Asp.Net路由系统,今天我们再来简单剖析一下Asp.Net Web API以WebHost方式部署时,Asp.Net Web API的路由系统内部是怎样实现的.还是以一个简单实例开 ...
- SwaggerUI ASP.Net WebAPI2
目前在用ASP.NET的 WebAPI2来做后台接口开发,在与前台做测试的时候,总是需要发送一个demo给他,但是这样很麻烦的,他还有可能记不住. 然后就想到SwaggerUI 生成测试文档. 话不多 ...
- 一个Python小白5个小时爬虫经历 【续】
前言 昨天实现了python简单的数据采集之后本来还挺高兴的,结果发现在.NET读取txt文件后反序列化总是报错.具体错误原因好像是从txt读取数据之后会自动加一个隐藏的字符串,没错,肉眼看不见,就导 ...
- 二cha树
void porder(BTree *b) { BTree *St[MaxSize],*p; ; if(b!=NULL) { top++; St[top]=b; ) { p=St[top]; top- ...
- AMD的cpu如何安装Mac OS
AMD的cpu如何安装Mac OS 经过一个月的折腾,终于成功的安装了自己的Mac系统,并且能够成功的启动.在这里分享一下自己安装的经验. 为了安装方便,有不影响原来的系统的使用 ...
- vim编译安装+lua模块
vim编译安装+lua模块 使用背景:代码自动补全插件,需要安装lua模块 安装准备,首先下载安装vim所依赖的其它安装包,ncurses,lua,readline,vim 源码下载,编译安装 ncu ...
- 关于ReentrantLock和Condition的用法
这篇博客是过年后的第一篇博客,2.13正式上班,之前在家休年假.上班第一天公司说有个紧急的项目需要上线,所以我们连续加了两个星期的班,直到上个周六还在加班,终于成功上线了.今天是2月的最后的一天,继续 ...
- 11g R2 RAC启动关闭步骤
1.关闭监听 /u01/app/11.2.0/grid/bin/srvctl stop listener -n redhat-rac01 /u01/app/11.2.0/grid/bin/srvctl ...
- 从零开始部署小型企业级虚拟桌面 -- Vmware Horizon View 6 For Linux VDI -- 概念简介
什么是桌面虚拟化? 桌面虚拟化有很多概念,此处谈论的,是指的一般企业使用的“服务器 + 虚拟机 + 云终端”的方式来实现的. 桌面虚拟化的原理是什么? 桌面虚拟化看上去高大上,实际上原理非常的简单.拿 ...