
简介中国蚁剑AntSword2.1.9 源码包面向从事 Web 安全测试、渗透测试学习以及工具二次开发的技术人员提供轻量级 Web 安全管理平台的完整工程实现便于研究远程连接管理、插件加载机制与图形界面交互等核心设计也可在更新源延迟时作为本地备用加载目录使用。压缩包共 2777 个文件体积约 15.51MB其中 1471 个 js 文件承载主要业务逻辑与第三方依赖175 个 json 与 58 个 yml 负责配置描述120 个 css、40 个 html 及若干 ts、coffee 文件对应界面与脚本204 个 md 文档及 license、authors、changelog 等文件保留了项目说明与开源协议另有 gif、png、svg、ttf、woff 等图标字体资源。目前已有 1621 人学习下载。读者可据此梳理清晰的源码目录层级理解 HTTP 交互、插件扩展与前端构建方式并在此基础上做定制开发和教学演示适合安全从业者与爱好者作为参考资料。1. 一次跨语言通道测试把我推到了 AntSword 源码面前前阵子做一次内部资产梳理面对的目标环境有 PHP、ASPX、JSP 三种桌面上的测试脚本越堆越多每换一种语言就要改一遍请求构造和返回解析编码方式还不一样来回切得人头晕。最后把这类重复劳动收进一个桌面工具的就是 AntSword 这套 Electron 架构。它真正值得拆的地方不在于界面而在于把「不同服务端语言的差异」全部压到模块层编码器负责传输形态模板负责最终字符串拼装界面层只关心渲染结果。这份 2.1.9 的源码包适合已经会写 Node、想搞清楚桌面端安全工具怎么做插件化分层的人也适合被「一套界面适配多种后端」这个命题卡住过的开发者。2. 技术栈与目录结构Electron 主进程加渲染进程的骨架2.1 技术选型为什么是 Electron 而不是浏览器扩展先说结论这套东西选 Electron 不是为了「好看」是被能力边界逼出来的。浏览器扩展受同源策略和 CORS 限制发出去的请求要过一遍预检很多自定义头部根本带不过去而 Electron 主进程跑在 Node 环境里HTTP 请求走的是 Node 的 net/http 模块没有同源概念也不受浏览器安全策略约束。再加上工具需要本地文件读写保存配置、导出结果、需要多窗口、需要系统托盘常驻这些能力浏览器扩展一样都给不了。具体的依赖栈大致是这么几层Electron整个应用的壳负责窗口、进程、菜单、更新。Node.js主进程的运行时所有网络请求、文件操作、模块加载都在这一层。React Redux渲染进程的界面框架和状态管理Shell 列表、连接状态、日志面板这些都是 React 组件订阅 Redux store 得到的。Webpack Babel把 JSX、ES Module 打包成 Electron 能直接加载的产物同时负责 dev 阶段的 watch。electron-builder或同类打包器把 dist 产物和 Electron 运行时打成一个可分发安装包。我一般会先打开package.json看dependencies里 Electron 的版本号因为它直接决定你本机要装哪个 Node 大版本——Electron 内嵌的 Node 和你系统里node -v出来的经常不是一回事这点后面会踩坑。2.2 目录结构source 下每一层在干什么不同小版本的目录会有出入但 2.1.9 这一代的分层大体是下面这个样子你拿到源码包后可以对着看antSword-2.1.9/ ├── source/ │ ├── app/ │ │ ├── main.js # Electron 主进程入口创建窗口、注册 IPC │ │ ├── package.json # 应用级依赖声明 electron 入口 │ │ └── renderer/ │ │ ├── index.html # 渲染进程的页面骨架 │ │ ├── index.js # 渲染进程入口挂载 React 根组件 │ │ ├── components/ # 通用 UI 组件列表、表单、弹窗 │ │ ├── modules/ # 业务视图shell 管理、设置、插件 │ │ └── assets/ # 图标、样式、字体 │ ├── modules/ # 核心按服务端语言划分的适配模块 │ │ ├── base.js # 模块基类 / 工厂定义统一接口 │ │ ├── php/ │ │ ├── asp/ │ │ ├── aspx/ │ │ ├── jsp/ │ │ └── custom/ │ ├── core/ # 全局配置、日志、状态持久化 │ ├── package.json │ └── webpack.config.js看这个结构要抓住一个关键点source/app和source/modules是两个独立的世界。前者是「界面 宿主」后者是「协议适配」两边只通过一层薄薄的接口打交道。source/modules下面每个语言目录内部又是同构的——index.js暴露模块主体encoder/和decoder/放该语言常用的变换template.js有的版本叫payload.js负责最终字符串的拼装。这种「目录即契约」的设计好处是你新增一种语言时不用动界面代码只要把目录照抄一份改实现。2.3 进程边界IPC 通道怎么把界面和 Node 能力连起来Electron 最容易被新手忽略的就是进程边界。渲染进程本质上是个浏览器环境默认拿不到require、fs、net想访问本地能力必须通过 IPC 往主进程发消息。2.1.9 这一代源码里这在main.js的webPreferences上有明确体现// source/app/main.js —— 主进程创建窗口并注册一条最小通道结构示意 const { app, BrowserWindow, ipcMain } require(electron); const path require(path); const fs require(fs/promises); let win; function createWindow() { win new BrowserWindow({ width: 1280, height: 800, webPreferences: { // 关键开关不开这个渲染进程里 require 直接抛 undefined nodeIntegration: true, // 2.x 时代普遍关掉上下文隔离方便渲染层直接引 Node 模块 contextIsolation: false } }); win.loadFile(path.join(__dirname, renderer/index.html)); } // 渲染进程侧ipcRenderer.invoke(fs:read, filePath) ipcMain.handle(fs:read, async (_event, filePath) { // 简单校验避免渲染层传进来任意路径 if (typeof filePath ! string || !filePath.endsWith(.json)) { throw new Error(只允许读取 .json 配置文件); } return await fs.readFile(filePath, utf8); }); app.whenReady().then(createWindow);这段代码里三个参数值得单独说。nodeIntegration: true决定了渲染进程能不能直接require这是整套架构成立的前提contextIsolation: false是配套的隔离开着的时候 preload 脚本和页面共享不了全局对象ipcMain.handle注册的是「请求-响应」式通道跟老的ipcMain.on那种单向广播不是一回事前者能await写起来更像普通函数调用。渲染进程那边对应的写法是// source/app/renderer/index.js 里的调用方式结构示意 const { ipcRenderer } window.require(electron); async function loadConfig() { try { const raw await ipcRenderer.invoke(fs:read, ./config.json); return JSON.parse(raw); } catch (e) { // 通道报错会以 rejected promise 形式回来别用 try 包住忘了打日志 console.error([ipc] fs:read failed:, e.message); return {}; } }注意这里用的是window.require而不是直接require因为在渲染进程里 Node 注入的require挂在 window 上。这套写法在 Electron 12 之后逐步被 preload contextBridge 取代但 2.1.9 这个年代的源码基本都是老路子你照原样跑没问题想升级再另说。3. 核心模块拆解Shell 模块、编码器链与模板的三层设计3.1 统一接口每种语言为什么都要实现同一套方法整个项目最值得学的就是这个抽象。不管后端是 PHP 还是 JSP界面层看到的永远只有几个动作列出目录、读文件、写文件、执行命令。这些动作在 UI 上是一致的但落到不同语言上拼出来的字符串完全不一样。AntSword 的解法是定义一个基类把所有差异压进子类的build()里// source/modules/base.js —— 所有语言模块的基类结构示意 class ShellModule { constructor(opt {}) { this.opt opt; // { url, pwd, type, encode, decode, headers } this.encode opt.encode || default; // 编码器链逗号分隔如 base64,reverse this.decode opt.decode || default; // 解码器链顺序必须与 encode 镜像 } // 子类必须实现把一次「逻辑动作」翻译成目标语言能接收的字符串 // 返回 { payload, headers }payload 是请求体headers 是需要覆盖的头 build(action, params) { throw new Error(build() must be implemented by subclass); } // 统一执行入口构造 - 编码 - 传输 - 解码 - 归一化 async exec(action, params) { const raw this.build(action, params); const body this.applyEncoders(raw.payload); const resp await this.transport(body, raw.headers); return this.applyDecoders(resp); } // 传输层单独抽出来方便替换成不同的 HTTP 客户端 async transport(body, headers {}) { const http require(http); // 实际实现会带超时、重试、cookie 处理这里只保留主干 return new Promise((resolve, reject) { const req http.request(this.opt.url, { method: POST, headers: { Content-Type: application/x-www-form-urlencoded, ...headers }, timeout: 15000 }, (res) { let buf ; res.on(data, (c) (buf c)); res.on(end, () resolve(buf)); }); req.on(error, reject); req.on(timeout, () req.destroy(new Error(request timeout))); req.write(body); req.end(); }); } } module.exports ShellModule;这里的action是个字符串标识比如list、read、write、execparams是业务参数对象。之所以用字符串而不是枚举是因为这套接口还要给插件层留扩展位——第三方模块可以自定义动作名界面层通过注册表查表分发不用改核心代码。transport()单独抽出来也是这个道理有人想换成 axios 或者带自定义证书的 https 客户端覆写这一个方法就够了。3.2 编码器与解码器链一层层剥离的字符串变换编码器这块是整个项目里最容易被低估、也最容易出问题的部分。它把「请求体长什么样」和「请求体怎么变形」彻底解耦了。配置文件里encode: base64,reverse这样的字符串表示发出去之前先做 base64、再整体反转decode那边就必须是reverse,base64顺序严格镜像。// source/modules/base.js 里编码链的实现结构示意 applyEncoders(data) { const names (this.encode || ).split(,).filter(Boolean); let out data; for (const name of names) { const mod require(../modules/${this.opt.type}/encoder/${name}); // 编码器接口约定encode(input, opt) string out mod.encode(out, this.opt); } return out; } applyDecoders(data) { const names (this.decode || ).split(,).filter(Boolean); let out data; for (const name of names) { const mod require(../modules/${this.opt.type}/decoder/${name}); // 解码器接口约定decode(input, opt) string out mod.decode(out, this.opt); } return out; }单个编码器的实现通常就是一个纯函数不碰 IO、不留状态// source/modules/php/encoder/base64.js结构示意 exports.encode (input) Buffer.from(input, utf8).toString(base64); exports.decode (input) Buffer.from(input, base64).toString(utf8);参数说明一下input永远是字符串编码器内部自己决定要不要走 Bufferopt里带着当前连接的完整配置有些编码器要靠opt.pwd做前缀校验。这套接口最大的好处是可组合——你想加一层「把空格替换成特殊字符」的混淆写一个新文件扔进encoder/目录配置里把名字加进去就生效了不用改动任何已有代码。我一般会把decode的第一个编码器做成「剥离前后缀」的清洗器因为实际响应里经常带着 HTML 噪音或者 BOM 头不先洗掉后面全乱。3.3 模板层最终字符串是怎么拼出来的模板是最贴近业务的一层也是最不该被塞进逻辑的一层。它的职责只有一个把「要执行的动作」包进目标语言能接受的外壳里。理想状态下模板就是个纯字符串函数入参出参都是 string中间不做任何判断。// source/modules/php/template.js结构示意外壳用占位符表示 module.exports { // mark: 本次请求的唯一标记用于响应里定位结果片段 // code: 真正要跑的代码片段由 build() 生成 // pwd: 当前连接口令有的语言需要通过它做入口定位 wrap: (mark, code, pwd) /*${mark}*/[${pwd}]${code}[/${mark}] };mark这个参数很关键。因为响应回来的是一个完整页面怎么从中精确抠出自己那次请求的结果靠标记做前后缀定位正则匹配/*mark*/([\s\S]*?)/*mark*/就能拿到。如果把这个逻辑散在编码器里链式变换一多标记就被改得面目全非了所以它必须放在模板这一层、在编码之前生成。code的来源是build()。以「列目录」动作为例PHP 模块的build()大致会生成一段读取目录并序列化输出的片段然后再交给模板套壳、交给编码器变形。三层各管一件事改任意一层都不影响另外两层——这就是这套架构真正的价值。3.4 界面层的数据流React 组件怎么拿到 Shell 状态界面侧走的是标准 Redux 单向流。Shell 列表存在 store 里每个 Shell 对象带着id、url、type、status、encode、decode这些字段侧边栏组件订阅列表右键菜单触发 actionreducer 里更新状态视图自动重渲染。// source/app/renderer/modules/shell/reducer.js结构示意 const initialState { list: [], active: null }; export default function shellReducer(state initialState, action) { switch (action.type) { case SHELL_ADD: // payload: { id, url, type, encode, decode, status: offline } return { ...state, list: [...state.list, action.payload] }; case SHELL_STATUS: // payload: { id, status } return { ...state, list: state.list.map((s) (s.id action.payload.id ? { ...s, status: action.payload.status } : s)) }; case SHELL_ACTIVE: return { ...state, active: action.payload }; default: return state; } }参数上没什么花活重点是SHELL_STATUS这个动作——每次连接成功或失败都要派发一次用来驱动界面上的小圆点变色。调试的时候我经常在这里加一行console.log因为连接失败的真正原因超时、编码不匹配、口令错误往往在这一层被吞掉了日志面板只显示一句「连接失败」查起来很痛苦。4. 把开发环境跑起来从 npm install 到窗口出现4.1 环境准备Node 版本要和 Electron 对上这一步看着简单翻车率最高。Electron 内嵌的是自己的 Node 运行时但你本地跑npm install用的是系统 Node两者版本差太多会导致某些原生依赖编译失败。2.1.9 这个年代的 Electron 大概是 8 到 11 之间对应的系统 Node 建议在 12 到 14 这个区间。# 先看当前环境 node -v npm -v # 有 nvm 的话切成 14 nvm install 14 nvm use 14 node -v # 确认输出 v14.x如果你的机器上已经是 Node 18 或 20npm install大概率会在node-sass或者electron-rebuild这类包上炸掉报gyp ERR!或者Python not found。这不是源码的问题是版本错配。切版本是最省事的解法别硬编译。4.2 依赖安装Electron 二进制是最容易卡住的一步进入source目录再装不要在仓库根目录装——根目录可能没有package.json或者有一份只声明了元信息的。cd antSword-2.1.9/source # 用国内镜像源主要为了 Electron 那个几十 MB 的二进制 npm install --registryhttps://registry.npmmirror.comnpm install期间会触发 Electron 的 postinstall去下载对应平台的预编译二进制。这一步常见的现象是卡在node_modules/electron不动或者装完npm start报Electron failed to install correctly。原因基本可以确定是二进制没下全但npm认为装完了。解决办法是先把node_modules/electron删掉重装rm -rf node_modules/electron npm install electron --registryhttps://registry.npmmirror.com装完之后可以验证一下node_modules/electron/dist/目录下应该有可执行文件path.txt里写着对应平台的目标名。4.3 开发模式启动watch 和 electron 要分开跑源码里的脚本一般分两类一类是构建一类是启动。具体名字看package.json的scripts常见形态是{ scripts: { dev: webpack --watch --mode development, build: webpack --mode production, start: electron . } }开发的时候开两个终端一个跑npm run dev让 webpack 持续编译渲染进程代码另一个跑npm start起 Electron 窗口。# 终端 A npm run dev # 终端 B npm start先跑 A 再跑 B 是有讲究的webpack 得先把renderer/index.js编译出产物index.html里引的那个 bundle 才存在否则窗口起来是白屏控制台报 404。我第一次跑的时候就踩了这个盯着白屏看了十分钟最后发现是自己顺序反了。窗口起来之后主进程的调试靠--inspect# 主进程调试然后在 chrome://inspect 里连 npm start -- --inspect5858渲染进程的调试直接CtrlShiftI打开 DevTools 就行跟普通网页没区别。两边分开调这一点很重要因为渲染层报的错和主进程报的错完全是两套堆栈。4.4 构建与打包dist 目录是怎么来的npm run build走的是 webpack 的生产模式输出到dist/或者build/看webpack.config.js里的output.path。这一步只负责把 JS、CSS、HTML 打成一坨静态资源还不足以变成可执行的安装包。打包要再走一层通常是 electron-builder# 生成当前平台的可执行目录不生成安装包调试用 npx electron-builder --dir # 生成安装包 npx electron-builder --win --x64--dir这个参数我强烈建议先用一次。它不做 NSIS 封包只把产物摊在dist/win-unpacked/下双击里面的 exe 就能跑。这样出问题能直接看到是哪一步挂了直接打安装包的话失败信息往往只有一句build failed非常难查。配置项在package.json的build字段或者单独的electron-builder.yml里files决定哪些文件进包asar决定要不要压成一个归档文件。5. 避坑与排查跑源码时最容易翻车的五个地方5.1 Electron 装不上npm start 报二进制缺失现象npm install显示成功npm start直接报Error: Electron failed to install correctly, please delete node_modules/electron and try installing again。原因postinstall 阶段的二进制下载被中断但 npm 的缓存已经记下了这次「安装完成」所以后续不会重试。解决删掉node_modules/electron和~/.npm/_cacache里对应的条目然后单独重装。装完检查node_modules/electron/path.txt是否存在这个文件缺失基本就是没装好。另外注意别在NODE_ENVproduction下装那样会跳过 devDependencies而 electron 恰好常常就放在 devDependencies 里。5.2 渲染进程里 require is not defined现象窗口能起来但界面一片空白DevTools 控制台第一行就是Uncaught ReferenceError: require is not defined。原因webPreferences里的nodeIntegration是false或者contextIsolation是true。新版 Electron 默认都往安全侧靠老代码跑在新壳子上就会这样。解决确认main.js里创建窗口时显式设置了nodeIntegration: true和contextIsolation: false。如果已经设了还报错检查是不是有多个BrowserWindow创建点你改的那个不是真正加载页面的那个——这种情况在带 splash 启动页的项目里很常见。5.3 打包后静态资源 404开发环境却正常现象npm start一切正常electron-builder --dir出来的包一跑图片、字体全裂控制台一堆net::ERR_FILE_NOT_FOUND。原因开发时路径是相对当前工作目录算的比如./assets/logo.png打包后文件被塞进app.asar归档里工作目录变了相对路径就指错了地方。解决所有资源路径统一走path.join(__dirname, ...)或者app.getAppPath()不要手写相对路径。如果确实有文件不想进 asar比如需要外部工具读取的在build配置里用asarUnpack单独放出来然后从process.resourcesPath下去找。上线前一定用--dir出来的包完整点一遍别只看开发环境。5.4 改了主进程代码窗口起来还是旧的现象改完main.js里的 IPC 处理逻辑重启npm start行为完全没变。原因webpack --watch只盯着渲染进程的入口主进程代码根本没进编译流程Electron 直接把旧的main.js拿去跑了更隐蔽的一种是产物被 webpack 复制到了dist/但package.json的main字段指向的是源码目录两边不同步。解决先确认package.json的main到底指向哪个文件然后看那个文件的内容是不是你刚改的。主进程代码改动必须手动重启 Electronwatch 救不了。我一般的做法是在main.js顶部加一行console.log([main] boot at, new Date().toISOString())每次启动都能一眼确认是不是新代码在跑。5.5 编码链顺序写反响应全是乱码现象连接能通但返回的结果里夹杂大量不可读字符或者 JSON 解析直接抛Unexpected token。原因encode和decode的顺序没有镜像。请求侧是「先 base64 再反转」响应侧就必须「先反转再 base64」写成一样的顺序会导致字符被错位还原。解决在applyEncoders和applyDecoders里各加一行中间值打印对比请求发出前和解码完成后两端的字符串。如果第一层解码出来的东西就已经不对说明顺序反了如果第一层对、后面才错说明某个编码器内部实现和配置里的名字对不上。这个坑我踩过两次第二次是因为复制配置时把decode那行漏改了肉眼扫了三遍才看出来。6. 二次开发改造从零接一个自定义编码器真要上手改我建议从「加一个编码器」开始改动面最小、见效最快而且能把整条链路跑一遍。假设你想在传输前对请求体做一层字符替换把空格换成%20之外的某种占位符绕开某些中间层对空格的过滤。第一步建文件路径跟随语言目录比如source/modules/php/encoder/space.js// 编码器接口约定纯函数两个方法不碰 IO exports.encode (input) String(input).replace(/ /g, __SP__); exports.decode (input) String(input).replace(/__SP__/g, );第二步是在配置里启用。这套东西的编码器列表是逗号分隔的字符串所以直接在连接的encode字段里追加名字即可注意decode那一侧也要加同名项位置要对应// 连接配置对象示意 const shellOpt { url: http://127.0.0.1:8080/index.php, pwd: demo, type: php, encode: base64,space, // 先 base64再替换空格 decode: space,base64 // 反过来先还原占位符再 base64 解码 };第三步是让界面能选到它。如果源码里的下拉框是硬编码的选项数组就在那个数组里补一项{ label: 空格替换, value: space }如果它做了目录扫描那就什么都不用改重启后会自动出现。这两者怎么区分看下拉框的数据来源是require进来的常量还是运行时读目录一目了然。验证方法别用真实环境本地起个假的回显服务最省事// 本地回显服务把收到的请求体原样返回用来验证编码是否正确 const http require(http); http.createServer((req, res) { let body ; req.on(data, (c) (body c)); req.on(end, () { console.log([recv], JSON.stringify(body.slice(0, 200))); res.end(body); }); }).listen(8080, () console.log(echo server on :8080));跑通之后日志里应该能看到请求体里全是__SP__而界面上的解码结果又是正常文本。这一步走通说明编码链、传输层、解码链三段都是活的再去改更复杂的逻辑心里就有底了。几条实操上的经验新编码器一定要做幂等性检查decode(encode(x)) x用一个循环跑一百次随机字符串能挡掉绝大部分低级错误不要在没有本地回显验证的情况下直接接真实环境编码链错了返回的是乱码你根本分不清是编码问题还是网络问题还有改完encoder/目录下的文件必须重启 Electron这些模块是require进来的Node 有模块缓存热更新到不了这一层。从那以后我每次新增编码器都会先把那段幂等性测试跑一遍再去看界面——血泪经验省下来的时间比写测试多得多。希望帮到你。本文还有配套的精品资源点击获取