前言
CEF 是 Chromium Embedded Framework 的简写,这是一个把 Chromium 嵌入其他应用的开源框架。
现在市面上有许多桌面软件都使用了CEF框架,比如我们经常使用的某钉、某云音乐等等。
我本意是突破某钉的一些功能限制,结果发现某钉使用了CEF框架,故开始对CEF框架做了一些浮于表面的探索。由于个人能力有限,如果文章中有什么错误之处,还望大家多多指教。
二
初探
在开始正式开始之前,有必要先观察一下某钉的安装目录,看看里面有哪些我们感兴趣的文件。
我电脑上的某钉版本是6.5.30-Release.7289101。
通过查看运行中的DingTalk.exe进程的映射文件锁定你电脑上目前运行的某钉的目录(这个地方会发现有多个同名进程,我们随便选择一个)。
有朋友可能要问为什么要通过这种方式确定目录,这其实是因为某钉的安装目录下面一般都会存在两个版本的文件,一个是当前版本另外一个则是上一个版本。据我观察这两个目录下的文件结构基本一致。
我电脑上的某钉目前就使用的是current目录。
打开current目录可以发现许多的资源文件和依赖库文件,其中对于本文来说最重要的文件是libcef.dll和web_content.pak。libcef.dll是CEF框架的支持库,web_content.pak则是某钉缓存在本地的html、js、css文件。
web_content.pak本质是一个zip压缩文件,我们可以通过解压软件查看里面的内容。
那么可以知道这个压缩文件是被加密了,解压的时候会让输入密码,后面会提到怎么获取密码。通过观察文件的名字也大致可以猜出这些文件的作用。
某钉中使用CEF框架的区域主要在聊天框显示区域。
下面主要介绍三个方面的内容:
CEF框架部分API和数据结构的介绍;
web_content.pak文件解密;
在某钉中开启CEF框架内置的调试窗口。
另外提一嘴,在某钉的安装目录下面我们还可以发现有cef_LICENSE.txt``duilib_license.txt等license声明,通过这些声明我们也可以获得一些信息,比如钉钉还使用了duilib界面库。
三
环境准备
既然某钉使用了CEF框架,那么学会简单的使用CEF框架,了解相关的API会使我们事半功倍。
根据官方库的指引,我们前往 https://cef-builds.spotifycdn.com/index.html
下载框架。
官方实现了C语言版本的CEF框架以及C++版本的CEF框架,其中C++版本的框架是基于C语言版本的二次封装。而我们需要的libcef.dll就是C版本的框架。
在此处下载的文件包含了已经编译好的libcef.dll,无需我们从源码编译libcef库。
实质上从源码编译libcef库并不容易,因为其中涉及到编译chromium,我猜这也是为什么官方会提供各种平台各种版本的库的原因吧。
在下载时我们需要先了解CEF的版本编号格式。
格式解释如下:
以cef_binary_104.4.25+gd80d467+chromium-104.0.5112.102_windows32.tar.bz2为例,其中
104.4.25和104.0.5112.102是CEF和Chromium的版本信息,gd80d467是git commit的hash。
我们可以先看看某钉使用的libcef.dll是什么版本。
这里发现一个很坑的点,就是Windows的文件属性显示不全,而且还不能拖开,也不能复制。
不过根据已经显示出来的内容,可以发现某钉使用的libcef.dll明显不是在官方提供的页面下载的。版本约定和官方的太不一样,git commit是8位的,官方库可是只有7位。
g2e1fb6b,我尝试使用g2e1fb6、2e1fb6b等hash在commit列表中搜索也没有发现,只能猜测某钉使用的libcef.dll是自己从源码编译的,而且可能对源码做了一些修改吧。
同时我使用91.0.0在下载界面搜索也没有发现相同的版本。后面的版本信息显示不全,得想个办法解决一下子,争取下载一个最接近的版本。其实这里有一个大坑,后面会提到。
其实文件属性的信息是存在于PE中的资源节中的,使用Windows系统提供的API或者自己解析都可以拿到相关信息。不过我是本着能不写代码就不写代码的懒人思想的。
一般这种库或者框架的动态库中都会提供函数查询版本信息,所以我浏览了一下libcef的导出函数。
在libcef的导出函数中我发现了cef_version_info这个函数,看名字就知道干什么用的了。
该说不说,官方提供了C++版本的文档,为什么不提供一个libcef的api文档呢?反正我是没找到。不过虽然没有文档,还是有源码和大量注释的。
这个函数的定义是这样的:
int cef_version_info(int entry);
我们再结合下面的信息。
从反汇编很明显的看出来这是一个数组下标寻址。
从源码得知不同的参数获取不同的信息,那么完整的版本信息存在于一个32字节的数组中。
在内存窗口转到数组内存。
我们缺少的是最后Chromium的版本信息,那么就是最后四个int。那么简单的拼接,得到
5B.0.1178.A4 转成10进制 91.0.4472.164。
搜索发现只有一个版本满足要求,那么就用这个好了,下载Standard Distribution,这个里面的文件是完整的,包含了框架代码和示例代码。
后面突然想起使用解析PE的格式的一些工具,也能很方便的查看资源信息。
我用CFF试了一下。
将下载后的文件解压,使用cmake生成vs工程。然后使用vs编译。
这个时候编译成功了,当然可能会在编译的时候遇到一些错误或者警告,按照提示解决即可。
那么环境准备好了,我们需要去学习一些CEF框架的基础知识了,直接看示例代码或者直接看框架源码都不是那么容易的,可以先在网上找前辈取点经。
掘金小册-CEF 桌面软件开发实战( https://juejin.cn/book/7075387142121193502 )
知乎专栏- CEF( https://www.zhihu.com/column/c_1333096419650269184 )
四
基于某钉的实战
最终的目标是实现某钉聊天窗口的防撤回功能,基于这个目标,一步步的解决一些遇到的问题。
CEF可以从本地或者网络加载资源,一般来说桌面应用程序会将大部分需要用到的文件缓存在本地。
所以第一步就是需要找到资源文件的位置,这个不同的软件可能使用的资源文件的名称不太一样,存放的位置也不太一样。比如某钉是放在安装目录下的,但是网易云音乐就没有放在安装目录下。
在某钉登录页面附加DingTalk.exe。
选择没有命令行参数的附加。
选择这两个函数下断点
cef_stream_reader_create_for_data
cef_stream_reader_create_for_file
这两个函数是CEF提供的两个操作文件数据的函数,返回值都是cef_stream_reader_t结构体。
区别在于cef_stream_reader_create_for_file的参数是文件路径
cef_stream_reader_create_for_data的参数是内存地址和大小,即内存中的文件数据。
这两个函数的声明和相关的结构体如下:
///
// Structure used to read data from a stream. The functions of this structure
// may be called on any thread.
///
typedef struct _cef_stream_reader_t {
///
// Base structure.
///
cef_base_ref_counted_t base;
///
// Read raw binary data.
///
size_t(CEF_CALLBACK* read)(struct _cef_stream_reader_t* self,
void* ptr,
size_t size,
size_t n);
///
// Seek to th