基於 electron 實現簡單易用的抓包、mock 工具

muwooo發表於2021-07-01

背景

經常我們要去看一些頁面所發出的請求時,經常會用到 Charles 做為抓包工具來進行介面抓取,但一方面市面是很多抓包工具都是收費或者無法二次開發的。當前我們團隊大多數用的也都是 Charles,但是對於一般新手來說,單純想抓個包或者修改和介面返回資料,直接上手 Charles 不管配置成本和學習成本都相對較高。所以我們有必要自己按照自己最爽的狀態來擼一個適合自己的抓包工具。

結果

基於以上訴求,我們自己重新設計並修改了互動,搞了一個符合我們訴求,使用簡單的桌面端抓包工具。本身這個外掛是可以整合到 utools 裡面的,但是由於很多涉及到內部的功能,我們沒法通過 utools 進行釋出,所以我們自己做了一個可以使用 utools 所有生態的工具箱 Rubick。此次抓包工具也是基於 Rubick 的外掛工具。

試玩地址:

Rubick: https://github.com/clouDr-f2e/rubick

抓包外掛:https://github.com/clouDr-f2e/rubick-network

這次我們先不看程式碼,直接來看我們實現的抓包工具的效果:

支援HTTP/HTTPS請求抓取。

1624454215086_2E5F5BE8-7687-4301-A2A9-859674E5BA09.png

支援介面mock

只需要將需要mock 的介面,右擊加入 mock 即可完成對介面資料的mock動作,後續所有介面請求將會走到該mock場景:

image.png

支援代理轉發

image.png

如何使用

首先我們需要clone Rubick 工具箱,他是一個基於 Electron 的類似於 utools 的工具箱,
倉庫地址:Rubick: https://github.com/clouDr-f2e/rubick 然後可以本地執行:

npm i
npm run dev

之後我們會看到這樣的介面:

image.png

如果熟悉 utools 的同學,可以直接略過後面的步驟了。

然後再 clonerubick-network 外掛,一切準備就緒後,直接複製 rubick-network 下的 plugin.jsonrubick 輸入框 選擇新建外掛即可看到外掛資訊:

image.png

然後開啟外掛,即可進行搜尋使用!

image.png

相比 utools 而言,rubick 最大的優勢是開源!!! 歡迎社群一起建設完善 rubick

技術實現

以後我們再來介紹一下是如何實現這樣一款抓包代理工具的。在實現抓包代理工具之前,首先大家需要去自學一下關於 nodejs 實現代理 的一些知識點,這裡推薦幾篇不錯的文章:

HTTP 代理原理及實現(一)

HTTP 代理原理及實現(二)

簡單來講就是要實現一箇中間人,使用者通過設定代理,網路請求就會通過中間人代理,再發往正式伺服器。

這種中間人的實現方式有兩種。

一種為普通的HTTP代理,通過Node.js開啟一個HTTP服務,並將我們的瀏覽器或手機設定到該服務所在的ip和埠,那麼HTTP流量就會經過該代理,從而實現資料的攔截。

image.png

對於非HTTP請求,比如HTTPS, 或其他應用層請求。可以通過在Node.js 中開啟一個TCP服務,監聽CONNECT請求,因為應用層也是基於傳輸層的,所以資料在到達應用層之前會首先經過傳輸層,從而我們能實現傳輸層資料監聽。

image.png

但是對於CONNECT捕抓到的請求,無法獲取到HTTP相關的資訊,包括頭資訊等,這對一般的前端分析作用不大,那麼想要真正監聽HTTPS,還需要支援證書相關的驗證。

關於證書如何生成網上也有很多教程,這裡就不在贅述,可以自行百度。不過看了一圈別人的設計,自己再動手實現一個 http 代理服務就是輕而易舉的事情了,但是為了更加快捷的實現功能,這裡我們選擇了 anyproxy 作為基礎服務,站在巨人的肩膀上進行開發。

anyproxy 安裝後提供了一個 websocket 服務,我們只需要監聽 websocket 埠對代理過來的資料進行動態展示即可:

function initWs(wsPort = 8002, path = 'do-not-proxy') {
  if(!WebSocket){
    throw (new Error('WebSocket is not supported on this browser'));
  }

  const wsClient = new WebSocket(`ws://localhost:${wsPort}/${path}`);
  wsClient.onerror = (error) => {
    console.error(error);
  };

  wsClient.onopen = (e) => {
    console.info('websocket opened: ', e);
  };

  wsClient.onclose = (e) => {
    console.info('websocket closed: ', e);
  };

  return wsClient;
}

// 連結websocket
const connectWs = () => {
  const wsClient = ws.initWs();
  wsClient.addEventListener('message', (e) => {
    const data = JSON.parse(e.data);
    store.commit('addRecord', data.content);
  });
}

但是 anyproxy 僅僅提供 node 端的能力,所以正好 electron 可以使用 nodejs 能力,也就是我們可以藉助 electron 來實現 nodejs 能力。這裡由於我們是外掛化的,所以參考 utools 的設計,實現方式如下:

// preload.js
const AnyProxy = require('anyproxy');

const options = {
  port: 8001,
  webInterface: {
    enable: true,
    webPort: 8002
  },
  forceProxyHttps: true,
  wsIntercept: false, // 不開啟websocket代理
  silent: true
};

class Network {
  constructor() {
    this.mockList = [];
    this.proxyServer = null;
  }
  initNetwork(op, {
    beforeSendRequest,
    beforeSendResponse,
    success,
    onRecord,
  }) {
    if (op === 'start') {
      if (!this.proxyServer || !this.proxyServer.recorder) {
        const _this = this;
        options.rule = {
          *beforeSendRequest(requestDetail) {
            if (beforeSendRequest) {
              return beforeSendRequest(requestDetail);
            }
            return requestDetail
          },
          *beforeSendResponse (requestDetail, responseDetail) {
            if (beforeSendResponse) {
              return beforeSendResponse(requestDetail, responseDetail);
            }
            return responseDetail;
          }
        };
        this.proxyServer = new AnyProxy.ProxyServer(options);
        this.proxyServer.once('ready', () => {
          console.log('啟動完成');
          success && success(this.proxyServer);
        });
      }
      this.proxyServer.start();
    } else {
      AnyProxy.utils.systemProxyMgr.disableGlobalProxy('http');
      AnyProxy.utils.systemProxyMgr.disableGlobalProxy('https');
      this.proxyServer.close();
      success && success();
    }
  }
  getIPAddress() {
    const interfaces = require('os').networkInterfaces();
    for (const devName in interfaces) {
      const iface = interfaces[devName];
      for (let i = 0; i < iface.length; i++) {
        const alias = iface[i];
        if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
          return alias.address;
        }
      }
    }
  }
}

只需要在 preload.js 中加上 anyproxy 服務的實現,即可完成我們的訴求!

結語

什麼是 rubick ?

基於 electron 的工具箱,媲美 utools的開源外掛,已實現 utools 大部分的 API 能力,所以可以做到無縫適配 utools 開源的外掛。 之所以做這個工具箱一方面是 utools 本身並未開源,但是公司內部的工具庫又無法釋出到 utools 外掛中,所以為了既要享受 utools 生態又要有定製化需求,我們自己參考 utools 設計,做了 Rubick.

歡迎大家給rubick pr 和提 issue 幫助我們完善

Rubick: https://github.com/clouDr-f2e/rubick

相關參考:

HTTP 代理原理及實現(一)

HTTP 代理原理及實現(二)

Electron + Vue 實現一個代理客戶端

相關文章