yapi 二次开发--socket接口推送和抓包

yapi 二次开发

  1. yapi 二次开发 项目开发环境搭建 数据库配置
  2. yapi mock的原理和源码解析
  3. socket 拉取和推送业务梳理(二次开发)
  4. socket 抓包业务梳理(二次开发)
  5. 遇到的坑及解决方案

二次开发环境搭建

环境要求:

  1. node 版本 >= 7.6.0
  2. npm 版本 >= 5.0
  3. ykit 旧版本 0.x
  4. 确保本地安装了mongodb数据库 (我本地是v4.0.26)
  5. 只支持mac或者linux系统开发,不支持windows系统开发
二次开发
安装yapi
  1. 创建工程目录
mkdir yapi && cd yapi
git clone https://github.com/YMFE/yapi.git vendors --depth=1 # 或者下载 zip 包解压到 vendors 目录

复制代码
  1. 修改配置
cp vendors/config_example.json ./config.json # 复制完成后请修改相关配置
vi ./config.json
复制代码

配置如下,主要配置 MongoDB 数据库,以及 Admin 账号。

{
  "port": "3000",
  "adminAccount": "[email protected]",
  "timeout":120000,
  "db": {
    "servername": "127.0.0.1",
    "DATABASE": "yapi",
    "port": 27017,
    "authSource": ""
  },
  "mail": {
    "enable": true,
    "host": "smtp.163.com",
    "port": 465,
    "from": "***@163.com",
    "auth": {
      "user": "***@163.com",
      "pass": "*****"
    }
  }
}
复制代码
  1. 安装依赖
cd vendors
npm install  --registry https://registry.npm.taobao.org # 安装依赖
复制代码
  1. 初始化
npm run install-server  # 安装程序会初始化数据库索引和管理员账号,管理员账号名可在 config.json 配置
复制代码
  1. 启动项目
npm run dev
复制代码
mongodb数据库安装

macOS 建议使用Homebrew安装

brew tap mongodb/brew 
brew install [email protected]
复制代码

安装后会在你的mac指定位置创建以下几个目录。intel和M1不太一样

![image-20220304143826885](/Users/calvin/Library/Application Support/typora-user-images/image-20220304143826885.png) 运行mongodb服务有两种方式:

  1. 将mongodb作为macOS服务使用brew启动
	brew services start [email protected] #启动服务
	brew services stop [email protected] #关闭服务
复制代码
  1. 将mongodb作为后台进程运行
		mongod --config /usr/local/etc/mongod.conf --fork #intel处理器
		
		mongod --config /opt/homebrew/etc/mongod.conf --fork  #M1处理器
		
		db.shutdownServer({timeoutSecs: 60}); #关闭服务
复制代码

此外,你可以将mongodb添加到环境变量中, 我的终端是zsh

vim ~/.zshrc
export PATH="$PATH:/usr/local/Cellar/[email protected]/4.0.26/bin"
source ~/.zshrc
复制代码

**注:macOS Catalina 为了保证安全,根目录只有只读权限,而启动mongodb时默认的数据存放位置是/data/db, 但因为没有写权限,可以用mongod --dbpath指定其他位置 **

sudo mkdir -p /System/Volumes/Data/data/db  #创建目录
sudo chown -R id -un/System/Volumes/Data/data/db #给权限
mongod --dbpath=/System/Volumes/Data/data/db #启动服务
复制代码

yapi mock 原理

yapi实现mock主要是mock.js库和json-schema-faker库在sandbox(沙盒)模式中运行产生各种数据类型的随机数据。

  • mock.js是数据模拟生成器。 mock.js官方
  • json-schema-faker 是json-schema + fake data generator可生成假数据
  • sandbox 修复了远程命令执行漏洞用的是safeity代替node.js 官方标准库中vm库。 Safeify 可让 Node 应用安全的隔离执行非信任的用户自定义代码。
safeity原理

动态执行一段代码,evalFunction构造函数,因为eval和当前代码的作用域是一致的,可以修改当前作用域变量,有安全问题。由 Function 构造函数创建的函数不会创建当前环境的闭包,它们总是被创建于全局环境,因此在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被 Function 构造函数创建时所在的作用域的变量 而safeity则采用proxy和Function构造器结合,执行了代码在 sandobx 中找的到,以达到「防逃逸」的目的,使得执行非信任的用户自定义代码更加安全。

function evalute(code,sandbox) {
  sandbox = sandbox || Object.create(null);
  const fn = new Function('sandbox', `with(sandbox){return (${code})}`);
  const proxy = new Proxy(sandbox, {
    has(target, key) {
      // 让动态执行的代码认为属性已存在
      return true; 
    }
  });
  return fn(proxy);
}
evalute('1+2') // 3
evalute('console.log(1)') // Cannot read property 'log' of undefined
复制代码
沙箱机制

node.js 提供一个内建模块VM, 该模块允许在 V8 虚拟机上下文中编译和运行代码。JavaScript 代码可以立即编译并运行,也可以编译、保存并稍后运行。

const vm = require('vm');
const script = new vm.Script('m + n');
const sandbox = { m: 1, n: 2 };
const context = new vm.createContext(sandbox);
script.runInContext(context);
复制代码

但这个模块是不安全的,yapi 代码漏洞就是出于此。 在yapi漏洞修复前的高级mock中执行以下代码,会访问服务器的权限,导致了沙箱逃逸,存在安全问题。

const sandbox = this
const ObjectConstructor = this.constructor
const FunctionConstructor = ObjectConstructor.constructor
const myfun = FunctionConstructor('return process')
const process = myfun()
mockJson = process.mainModule.require("child_process").execSync("whoami && ps -ef").toString()
复制代码

safeity在vm2的基础上作了进一步的处理,通过进程池统一调度管理沙箱进程。

vm safeity safeity特性:

  • 为将要执行的动态代码建立专门的进程池,与宿主应用程序分离在不同的进程中执行
  • 支持配置沙箱进程池的最大进程数量
  • 支持限定同步代码的最大执行时间,同时也支持限定包括异步代码在内的执行时间
  • 支持限定沙箱进程池的整体的 CPU 资源配额(小数)
  • 支持限定沙箱进程池的整体的最大的内存限制(单位 m)

yapi中使用的沙箱代码

const Safeify = require('safeify').default;

module.exports = async function sandboxFn(context, script) {
    // 创建 safeify 实例
    const safeVm = new Safeify({
        timeout: 3000,
        asyncTimeout: 60000
    })
    // fix error: TypeError: Cannot read property 'delay' of undefined
    // script += `\n return {mockJson, resHeader, httpCode, delay}`; 
    script += "; return this;";
    // 执行动态代码
    const result = await safeVm.run(script, context)
    // 释放资源
    safeVm.destroy()
    return result
}

复制代码

socket 拉取和推送业务梳理

背景:

在工程化项目的推动下,公司除了http接口还有socket接口,致力于前后端工作解耦,yapi只提供http接口的mock功能,需二次开发支持B/C端socket接口的mock以及接口文档管理

功能点:
  1. socket拉取和推送接口文档管理

  2. socket拉取接口mock功能

  3. socket推送接口mock功能及单次和定时推送(按一定频率推送)

技术栈:

前端 react redux 后端 koa mongoose

时序图

image-20220315090342602.png

socket 抓包业务梳理

背景:

基于fiddle、chales等抓包工具无法抓取socket接口,为便于测试人员快速定位问题,开发socket接口抓包功能

功能点:
  1. 流量列表和详情显示
  2. 支持正则和自定义内容、RequestMsgType/TopicId、ResponseMsgType/msgType筛选
  3. 每30s给服务端发一次心跳
  4. 监听页面移出 10min超时 自动关闭websocket连接通道
  5. 抓包服务管理
  6. 一键清除
  7. 断线重连
  8. 可手动暂停和重新建立当前连接
技术栈:

前端:react websocket webworker 后端:koa mongoose

遇到的坑及解决方案

  1. socket 按频率推送任务
  • 引入node-schedule 定时任务库的,并不能很好解决按一定频率推送需求,因为只能具体到秒。

  • 用定时器处理,存Map中(采用这个)

schedule.png

schedule2.png

  • 遇到的坑有:可以开启任务,但无法立即关闭当前任务。

原因:排查了很多,以为接口代码有错误,最终考虑到在部署yapi服务时开启了两个instance,导致开启时进入的instance1, 关闭时进入的是instance2,但两个instance并没有共享,是互相独立的。 解决方案:只开启一个instance在跑服务。

  1. socket 抓包
  • 长列表数据处理(虚拟列表),当一次性插入上千上万条数据到dom中,因为浏览器的对dom树渲染(重绘、回流)是非常耗硬件资源的,会导致渲染极慢,甚至会导致页面卡死崩溃。

虚拟列表原理:滚动加载数据时只加载可视区域的数据。 这边时直接引入了react-virtualized库。

  • 在使用react-virtualized时会下拉滚动条会触发react-virtualized table组件下defaultCellDataGetter.js 文件 代码报错 解决方案: 通过每次发版时npm装包后打补丁patch

hack.png

  • 虽然虚拟列表可以解决一次性渲染大量数据的问题,但抓包是通过websoket实时推送的,在短时间内频繁操作dom,使得cpu超负载,导致页面卡死崩溃,存在性能问题。

解决方案:参考了阿里开源项目any-proxy中web- workers开启多线程。

import RecordWorker from "worker-loader?inline!./CaptureWorker.js";
const myRecordWorker = new RecordWorker(window.URL.createObjectURL(new Blob([RecordWorker.toString()])));
复制代码
  • 困扰我很久的一个性能bug问题:定时器(setTimeout)滥用问题,在发websocket心跳时,在建立ws通道时,每发来一条数据就调用了心跳函数。因为在计算机底层cpu调度机制中,timer是通过一个优先级队列和一个Event Loop线程来实现定时功能。但大量事件插进队列后,无法即使释放导致cpu处理不过来,使得页面卡死。

img

解决方案:在ws建立后执行一次定时器(setInterval)来做心跳包。

heartbeat.png

猜你喜欢

转载自juejin.im/post/7080071920116301854