browsermob-proxy, 基于Java的代理服务
1:基础介绍
browsermob-proxy 以下在文章简称BMP。
BMP的具体流程有点类似与Flidder或Charles。即开启一个端口并作为一个标准代理存在,当HTTP客户端(浏览器等)设置了这个代理,则抓取并有能力修改所有的请求细节并获取返回内容。
BMP可以将HTTP请求细节数据导出到HAR文件
HAR(HTTP档案规范),是一个用来储存HTTP请求/响应信息的通用文件格式,基于JSON。这种格式的数据可以使HTTP监测工具以一种通用的格式导出所收集的数据,这些数据可以被其他支持HAR的HTTP分析工具(包括Firebug、httpwatch、Fiddler等)所使用,来分析网站的性能瓶颈。
|
BMP是基于LittleProxy,而LittleProxy又是基于Netty。
BMP有两种模式,嵌入式模式是利用Java代码来启动代理,并通过Java代码来截取修改请求获取内容。另一种是独立启动模式,可以通过命令行来启动,通过RestAPI来进行操作(https://github.com/lightbody/browsermob-proxy#rest-api)。
本文只讨论嵌入式模式。嵌入式模式可以跟Selenium很好的集成起来。
2:环境搭建
<dependency>
<groupId>net.lightbody.bmp</groupId>
<artifactId>browsermob-core</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
|
在resources目录下加入log4j.properties
log4j.rootLogger = INFO, stdout
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %d %p [%c] - %m%n
注:
一定要配置好日志环境,否则系统出错了完全看不出来。血泪教训。
在我配置时候,BMP2.1.4 和 Guava22.0 是不兼容的,导致服务起来之后,浏览器始终无法打开页面。然而,没有任何迹象表现出代理没有起来,或者运行过程中有错。
多方Google无果,因为完全没有日志记录。只能搜索 “browsermob not working” 或者 “browsermob connection reset”等等毫无意义的现象。然而什么都没有搜出来。
最后,配好了日志环境,发现了错误。
java.lang.NoSuchMethodError:com.google.common.net.HostAndPort.getHostText()
经过搜索发现,实际上是由于BMP官方兼容性问题导致。
|
BrowserMobProxy proxy =
new BrowserMobProxyServer();
proxy.start(
8180, InetAddress.getByName("localhost")
);
// 不要用常见的 8888 被 Charles用啦
Thread.sleep(10000000);
proxy.stop();
|
这样就算起了好了一个代理服务器,端口是8888。
是不是非常简单。
代理启动之后,主线程可以Sleep一段时间,然后把代理关掉,这样就能够实现定时关闭代理的功能了。
实际上,还可以拦截请求,在某些请求完成之后关闭代理。
由于代理是单独线程,跟启动它的主线程没有关系,因此主线程Sleep不会影响代理的可用性。
|
注:为了测试代理的运行情况, 可以将log4j.rootLogger改为DEBUG模式,这样会默认输出每个请求的细节。
Firefox推荐使用ProxySwitcher, Chrome推荐SwitchyOmega来进行代理设置。
3:SSL配置
默认不配置的情况下,对于非SSL的页面是可以随便访问的。但是对于SSL站点,会出现不是私密连接的告警,甚至直接打不开。
通过官方文档,我们知道需要安装一个证书。
下载这个文件,存到系统中。在Mac下是打开“钥匙串访问”,将这个文件拖入其中。
然后双击打开证书详情,并选择始终信任。
然后就可以直接访问SSL站点了。不过这个证书用作测试可以的,长期信任并不是一个好做法。这是一个公开证书,很容易被他人盗用。所以比较合理的方式是生成自己的证书。有兴趣自己研究,链接在这里。找时间单独写SSL相关的原理。https://github.com/lightbody/browsermob-proxy/blob/master/mitm/README.md
但是,在Mac上,这个方法对Firefox是无效的。Firefox自己维护了一系列证书。所以需要单独为其导入。这个特性其实很有用,即单独在FF上信任证书,测试的时候用FF,全局则可以不信任这个测试证书。然而,54.0 (64 位)版本的FF,即便加入了并信任了该证书,依旧无法访问SSL。为此,暂时没有Google到办法。只能弃用了。
不管怎么说,导入的办法如下:
首选项 -> 高级 -> 证书 -> 查看证书 -> 导入,选中证书文件之后,选择下面的三个信任,然后确定。
4:拦截请求
BrowserMobProxy proxy = new BrowserMobProxyServer();
proxy.start(8100, InetAddress.getByName("localhost")); // 不要用常见的 8888 被 Charles用啦
|
RequestFilter
proxy.addRequestFilter((request, contents, messageInfo) -> {
if (request.getMethod().equals(HttpMethod.POST)) {
System.out.println(request.getUri() + " ######### " + contents.getTextContents());
}
System.out.println(request.getUri() + " --->> " + request.headers().get("Cookie"));
return null;
});
RequestFilter 是一个接口,只有一个方法
HttpResponse filterRequest(HttpRequest request, HttpMessageContents contents, HttpMessageInfo messageInfo);
当这个方法在Proxy中被调用的时候,request参数包括了HTTP method, URI, headers等等。这些都是可以修改的。
当POST方法等提请求带有参数的时候,content中可以取到参数详情。content可以通过
HttpMessageContents#setTextContents(String) 或者 HttpMessageContents#setBinaryContents(byte[]) 来进行修改。
对于 request 和 contents 都会直接反映在最终给服务器的请求上。
如果返回值不是null, 那么代理不再往外发送请求,而是直接将这个非空的元素返回去给浏览器。
|
ResponseFilter
proxy.addResponseFilter((response, contents, messageInfo) -> {
System.out.println(
messageInfo.getUrl() + " >>>>>> " + response.getStatus() + " : " +
response.headers().get("cookie") + " | " + contents.getTextContents()
);
});
ResponseFilter是一个接口,只有一个方法 。
void filterResponse(HttpResponse response, HttpMessageContents contents, HttpMessageInfo messageInfo);
当这个方法在Proxy中被调用的时候,response参数包括了URI, headers, status line等等。
contents是返回的真正内容,可以通过以下方法来进行修改。
HttpMessageContents#setTextContents(String) 或者 HttpMessageContents#setBinaryContents(byte[])
对response和content的修改,都会最终反映到请求发起方。
|
存储HAR文件
// create a new HAR with label
proxy.newHar("bmp”);
// enable more detailed HAR capture, if desired (see CaptureType for the complete list)
proxy.enableHarCaptureTypes(
CaptureType.REQUEST_HEADERS, CaptureType.REQUEST_CONTENT, CaptureType.REQUEST_BINARY_CONTENT, CaptureType.REQUEST_COOKIES,
CaptureType.RESPONSE_HEADERS, CaptureType.RESPONSE_CONTENT, CaptureType.RESPONSE_BINARY_CONTENT, CaptureType.RESPONSE_COOKIES);
// 手动输入验证码,然后继续爬取过程
while (true) {
Scanner scanner = new Scanner(System.in);
String cmd = scanner.next();
if(cmd.equals("quit")){
break;
}
}
try {
Har har = proxy.getHar();
File harFile = new File("/Users/wangqi/Desktop/bmp.har");
har.writeTo(harFile);
} catch (IOException ioe) {
ioe.printStackTrace();
}
proxy.stop();
在线工具http://www.softwareishard.com/ 或者 Charles 可以打开存储下来的HAR文件。
由于HAR需要用Charles打开,而Charles默认的代理端口是8888,当Charles开启,所以如果BMP使用8888,可能出现一只拿不到任何HAR的诡异情况。
而,实际上,启动过程中竟然么有报端口被占用,也真是诡异。
|
BMP会自动加上一个RequestHeader,为了减少服务器对代理的影响,需要删除该Header信息。
proxy.addLastHttpFilterFactory(new HttpFiltersSourceAdapter() {
@Override
public HttpFilters filterRequest(HttpRequest originalRequest) {
return new HttpFiltersAdapter(originalRequest) {
@Override
public HttpResponse proxyToServerRequest(HttpObject httpObject) {
if (httpObject instanceof HttpRequest) {
((HttpRequest) httpObject).headers().remove(HttpHeaders.Names.VIA);
}
return null;
}
};
}
});
|
5: Selenium WebDriver和BMP的联合使用
WebDriver常用与自动化测试,所以在测试过程中将每个请求细节都完整记录下来当然是极好的。因此可以将 WebDriver和BMP联合使用,存储成HAR格式(第一部分有说明)。
// start the proxy
BrowserMobProxy proxy = new BrowserMobProxyServer();
proxy.start(0);
// get the Selenium proxy object
Proxy seleniumProxy = ClientUtil.createSeleniumProxy(proxy);
// configure it as a desired capability
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability(CapabilityType.PROXY, seleniumProxy);
// start the browser up
WebDriver driver = new FirefoxDriver(capabilities);
// enable more detailed HAR capture, if desired (see CaptureType for the complete list)
proxy.enableHarCaptureTypes(CaptureType.REQUEST_CONTENT, CaptureType.RESPONSE_CONTENT);
// create a new HAR with the label "yahoo.com"
proxy.newHar("yahoo.com");
// open yahoo.com
driver.get("http://yahoo.com");
// get the HAR data
Har har = proxy.getHar();
|