Skip to content
This repository has been archived by the owner on Nov 12, 2018. It is now read-only.

网页版微信抓包+注入实现表情贴纸显示 #2

Closed
geeeeeeeeek opened this issue Feb 24, 2016 · 18 comments
Closed

网页版微信抓包+注入实现表情贴纸显示 #2

geeeeeeeeek opened this issue Feb 24, 2016 · 18 comments

Comments

@geeeeeeeeek
Copy link
Owner

geeeeeeeeek commented Feb 24, 2016

作者:Zhongyi Tong (geeeeeeeeek@github)

协议:知识共享-署名 (CC BY 2.5 AU)

提示: 这是最初的实现方式。#13@arrowrowe 提出了Angular注入的思路,取代了原先的DOM注入,极大地简化了逻辑并提升了性能。这份文档仅供参考。

由于微信协议变动,目前表情商店里的贴纸无法显示。

我们使用网页版微信登录时都会发现,聊天中的表情贴纸都无法显示,被替换成了一段[Sent a sticker. View on phone.]文本,提示你到手机上查看。原本以为微信在下发消息推送时根据终端做了手脚,但抓包的结果表明表情贴纸就在response中,只不过微信出于某些考虑,没有在前端解析出来。

受够了万年不更新bug一大堆的官方微信客户端,我最近用Electron封装了一个Mac和Linux下的WeChat客户端。做了很多本地化的工作,包括自适应窗口、docker上的消息计数,还有就是这个表情贴纸注入,效果如下图所示。由于还没有到production-ready的程度,所以先分享一篇技术文,介绍这个功能的实现。

qq20160224-0 2x

请求分析

为了知道收到的是不是贴纸,以及是什么贴纸,我们先来分析Web WeChat发起的网络请求。打开Chrome的DevTools,Network选项卡,只需要关注XHR请求即可。

qq20160224-1 2x

主要有下面两种请求:

  • GET https://webpush.weixin.qq.com/cgi-bin/mmwebwx-bin/synccheck

    消息的同步请求,HTTP长连接,默认30s或需要同步时返回。

  • POST https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsync

    上面synccheck请求返回时,也就是有新消息时,微信发起一个webwxsync请求。在这个请求的response中,包含了我们需要的新消息数量AddMsgCount、消息IdMsgId、消息内容Content等等有用的内容。

我们具体看看一个表情贴纸的消息Content是怎样的:

<msg><emoji fromusername = "wxid_********" tousername = "filehelper" type="2" idbuffer="media:0_0" md5="ead80e721af1faf2bb33054450c61a66" len = "80226" productid="com.tencent.xin.emoticon.bilibili" androidmd5="ead80e721af1faf2bb33054450c61a66" androidlen="80226" s60v3md5 = "ead80e721af1faf2bb33054450c61a66" s60v3len="80226" s60v5md5 = "ead80e721af1faf2bb33054450c61a66" s60v5len="80226" cdnurl = "http://mmbiz.qpic.cn/mmemoticon/dx4Y70y9XctRJf6tKsy7FibsxibBIEComianWNa3zMITfOiaztUNzrgjhg/0" designerid = "" thumburl = "http://mmbiz.qpic.cn/mmemoticon/dx4Y70y9XctRJf6tKsy7F01Cqy3ej686O49bu7YrDWyQ2VPADkeFMg/0" encrypturl = "http://emoji.qpic.cn/wx_emoji/CkibiaVE4Z5fdDEKPW3LEPhPBVFvmicEQhLDWMQNtO6yH7ReGPGtgT44g/" aeskey= "be4577207982f4d793f3586790930af8" ></emoji> </msg>

这就很清晰了,头部表明自己是个emoji,然后是一些CheckSum的数据,后面跟着的cdnurl是贴纸的gif资源,thumburl是贴纸的缩略图。理论上拿到这些资源之后,我们就可以在页面中显示贴纸了。

Electron中的抓包

先放一个无关的提醒,如果你想在Electron中嵌入第三方网页,直接用browserWindow.loadURL即可,不要使用官方提供的webview标签。webview标签带来的输入法错位、窗口resize性能问题对产品体验的影响非常严重。

Electron的WebContents提供了did-get-response-details事件,但回调中只能获取到Response Headers,无法获取到Response Body。因此我们需要寻找一些tricky的方法来解决这个问题。我采用的方案是Chrome Debugging Protocol,这其实就是我们日常使用的DevTools为Chrome Apps暴露的一套API。只不过我们平时使用它的UI来调试,现在使用文本命令。

在Electron中我们可以这样连接debugger [docs]

 try {
   browserWindow.webContents.debugger.attach("1.1");
 } catch (err) {
   console.log("Debugger attach failed : ", err);
 }

 browserWindow.webContents.debugger.on('detach', (event, reason) => {
   console.log("Debugger detached due to : ", reason);
 });

 browserWindow.webContents.debugger.on('message', (event, method, params) => {
   if (method == "Network.responseReceived" && params.type == "XHR") {
     // Code here.
   }
 });

 browserWindow.webContents.debugger.sendCommand("Network.enable");

首先注册debugger,然后通过Network.enable开启记录Network日志,接收Network.responseReceived事件,类似于我们在DevTools Network选项卡中看到的请求。查看请求详情需要额外发送一个指令:

debug.sendCommand("Network.getResponseBody", {
        "requestId": requestId
      }, (error, response) => {
        // Code here.
      });

剩余的工作就是用正则表达式和一些逻辑判断从请求中获取到cdnurl,这里不再赘述,可以参考我的代码

Electron中的页面注入

如何在Electron中向WebContents注入代码请参考[文档]。大致思路是在dom-ready和有新的贴纸消息到达时分别注入下面两行代码:

browserWindow.webContents.executeJavaScript(`injectBundle.initEmojiListJS()`);
browserWindow.webContents.executeJavaScript(`injectBundle.updateEmojiListJS('${JSON.stringify(emojiList)}')`);

我们在浏览器环境中维护一个window.emojiList对象,emojiList[msgId] = imageUrl。在DOM中找到div.js_message_bubbledata-cm标签中包含相应msgId的组件,注入background等CSS使其显示贴纸。

我们要对页面上所有可见的贴纸消息气泡执行上述逻辑,因此注入的时机非常重要。Web WeChat是一个由Angular构建的应用,聊天气泡的加载是一个动态的过程,每次切换聊天,甚至是滚动聊天消息时,都会不断有节点的移除和加载。如果新加入的节点有贴纸消息的话,我们就需要及时注入。最后,我采用了一个对性能有些影响的解决方案:

  • 新的贴纸消息到达时替换这个贴纸消息
  • 切换聊天时替换所有贴纸消息
  • 滚动聊天消息时替换所有贴纸消息

下面是注入部分的代码:

injectBundle.replaceEmojiMessageJS = (msgId, imageUrl, delay) => {
  setTimeout(()=> {
    let $bubble = $(`div.js_message_bubble:regex("${msgId}")`)
        .css('background', `url('${imageUrl}') no-repeat`)
        .css('background-size', '120px')
        .css('height', '120px')
        .css('width', '120px');
    $bubble.addClass('no_arrow');
    $bubble.find('pre').text('')
        .css('width', '120px');
  }, delay);
};

injectBundle.updateEmojiListJS = (newList)=> {
  newList = JSON.parse(newList);
  window.emojiList = $.extend(window.emojiList, newList);
  for (let msgId in newList) {
    injectBundle.replaceEmojiMessageJS(msgId, window.emojiList[msgId], 0);
  }
};

injectBundle.initEmojiListJS = ()=> {
  $.expr[':'].regex = (elem, index, match) => {
    var regex = new RegExp(match[3]),
        $elem = $(elem);
    return regex.test($elem.attr('data-cm'));
  };

  window.emojiList = {};
  $('a.title_name').on('DOMSubtreeModified', () => {
    for (let msgId in window.emojiList) {
      injectBundle.replaceEmojiMessageJS(msgId, window.emojiList[msgId], 0);
    }
  });
  $('.chat_bd.scroll-content').on('DOMNodeInserted', (ev) => {
    if (ev.timeStamp - injectBundle.timestamp > 50) {
      injectBundle.timestamp = ev.timeStamp;
      for (let msgId in window.emojiList) {
        injectBundle.replaceEmojiMessageJS(msgId, window.emojiList[msgId], 0);
      }
    }
  })
};

完整的项目请见electronic-wechat

@arrowrowe
Copy link
Contributor

先点赞. 然后, 似乎也可以有另一个解决方案, 增加 transformResponse, 收到消息时就把表情类消息锁定为图片类消息. 求意见~

@geeeeeeeeek
Copy link
Owner Author

@arrowrowe 如果可以直接替换那是最吼的!微信把ng-inspect也干了就没去细想注入angular……你可以发一个Pull Request,我需要先test一下……

@arrowrowe
Copy link
Contributor

re @geeeeeeeeek: See #13. 谢先~

@oblank
Copy link
Contributor

oblank commented Mar 30, 2016

请问如果想扩展公众号文章右侧顶部的按钮是否可行,比如想把公众号的文章直接分享到twitter 或是 保存到 evernote

@geeeeeeeeek
Copy link
Owner Author

@oblank 应该可行,你可以做个拓展。

@xream
Copy link

xream commented Apr 6, 2016

刚刚发现 emoji 的图片是 20x20 的很模糊...如果能替换成高清版的就好了...

<img class="emoji emoji1f639" text="_web" src="/zh_CN/htmledition/v2/images/spacer.gif">

@ripples-alive
Copy link
Collaborator

貌似现在有些表情能显示,有些显示不了,这是一个没显示的表情 sync 的返回的 Content:

"Content": "&lt;msg&gt;&lt;emoji 
fromusername = \"ihciah\" 
tousername = \"wxid_******\" 
type=\"1\" 
idbuffer=\"media:0_0\" 
md5=\"5f323f6de9b33feadfcebfb8a9aaf912\"
len = \"8790\" 
productid=\"\" 
androidmd5=\"5f323f6de9b33feadfcebfb8a9aaf912\" 
androidlen=\"8790\" 
s60v3md5 = \"5f323f6de9b33feadfcebfb8a9aaf912\" 
s60v3len=\"8790\" 
s60v5md5 = \"5f323f6de9b33feadfcebfb8a9aaf912\" 
s60v5len=\"8790\" 
cdnurl = \"http://emoji.qpic.cn/wx_emoji/bnweqIcxewJscYfHjM8P73FOkwfYIMMicsHsib6Dgia4uYb6zGkqhFNXA/\" 
designerid = \"\" 
thumburl = \"\" 
encrypturl = \"http://emoji.qpic.cn/wx_emoji/bnweqIcxewJscYfHjM8P73FOkwfYIMMicUwm37cbTvstwxBdMQdzCPw/\" 
aeskey= \"b918b9c8eb07d751daa16fdcf9dad8dd\" 
width= \"200\" 
height= \"57\" 
&gt;&lt;/emoji&gt; &lt;gameext 
type=\"0\" 
content=\"0\" 
&gt;&lt;/gameext&gt;&lt;/msg&gt;",

@Qusic
Copy link

Qusic commented Apr 11, 2016

看起来 /webwxgetmsgimg 接口现在对于购买的表情都返回空的200了。。只有自定义表情能显示了

@geeeeeeeeek
Copy link
Owner Author

@ripples-alive @Qusic 没错

微信为此还修改了服务端真是不容易……

@ripples-alive
Copy link
Collaborator

@geeeeeeeeek 我这个没显示是自定义的吧,那个 cdnurl 对应的图片我看了是和手机上看到的一样啊,不能直接显示么?

@Qusic
Copy link

Qusic commented Apr 11, 2016

@ripples-alive web微信的api和手机的不一样

@geeeeeeeeek
Copy link
Owner Author

@ripples-alive 现在什么情况,我发现官方的Web WeChat有些自定义表情能显示,有些不能。

image

@ripples-alive
Copy link
Collaborator

😂 完全不知道什么情况。。。我只是用的时候发现有些表情总是显示不出来就来吱一声 😳
原来是官方的就已经有问题了么。。。

@geeeeeeeeek
Copy link
Owner Author

但是商店里的贴纸真的无法显示了,准备撤离这个功能……

@tcstory
Copy link

tcstory commented May 1, 2016

我刚好也想做一个第三方的客户端,偶然看到你的项目,有点好奇的时,你是怎么处理消息的收发的,因为我没想过要在electron里面加载网页版的微信,其实我的想法是,抓取所有网页版微信的api借口,但是我发现这好麻烦的

@arrowrowe
Copy link
Contributor

re @tcstory See also Wechat API related stuff in GitHub.

@zzz6519003
Copy link

现在想实现显示自定义表情 可以么?

@pjincz
Copy link

pjincz commented Oct 3, 2018

今天抓了一下数据,现在后端返回里面直接把content都给干掉了,真是233333

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

9 participants