Whistle Plugin Development
Introduction
Whistle is a cross-platform web debugging proxy tool based on Node.js. It supports HTTP, HTTPS, WebSocket protocols and provides rich plugin mechanisms for extending functionality.
Understanding Whistle
Core Features
- Protocol Support: HTTP/HTTPS/WebSocket/Tunnel
- Rule Configuration: Flexible rule matching and processing
- Plugin System: Rich plugin ecosystem
- Visual Interface: Web-based management interface
- Multi-platform: Cross-platform support
Basic Usage
Install and start whistle:
# Install globally
npm install -g whistle
# Start whistle
w2 start
# Access web interface
# http://localhost:8899
Plugin Development Basics
Plugin Structure
A typical whistle plugin structure:
whistle.your-plugin/
├── package.json
├── index.js
├── lib/
│ ├── server.js
│ └── utils.js
├── public/
│ ├── index.html
│ ├── index.js
│ └── index.css
└── README.md
Package.json Configuration
{
"name": "whistle.your-plugin",
"version": "1.0.0",
"description": "Your whistle plugin description",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"keywords": ["whistle", "plugin"],
"whistleConfig": {
"hintList": ["your-plugin://"]
}
}
Entry Point (index.js)
module.exports = (server, options) => {
// Plugin initialization logic
console.log('Plugin loaded:', options);
// Return plugin handler
return {
// Request handler
requestHandler: (req, res, next) => {
// Process request
next();
},
// Response handler
responseHandler: (req, res, next) => {
// Process response
next();
},
// WebSocket handler
wsHandler: (ws, req) => {
// Handle WebSocket connections
}
};
};
Plugin Types
1. Protocol Plugin
Implement custom protocols:
// whistle.myprotocol
module.exports = (server, options) => {
server.on('request', (req, res) => {
const { originalReq } = req;
const protocol = originalReq.headers['x-whistle-rule-value'];
if (protocol === 'myprotocol://mock') {
res.end(JSON.stringify({
message: 'Mock response from custom protocol'
}));
}
});
};
Usage in whistle rules:
example.com myprotocol://mock
2. UI Plugin
Create management interfaces:
// lib/server.js
const express = require('express');
const path = require('path');
module.exports = (server, options) => {
const app = express();
// Serve static files
app.use(express.static(path.join(__dirname, '../public')));
// API endpoints
app.get('/api/config', (req, res) => {
res.json({ status: 'ok' });
});
app.post('/api/config', (req, res) => {
// Handle configuration updates
res.json({ success: true });
});
return app;
};
3. Inspection Plugin
Analyze and modify requests/responses:
module.exports = (server, options) => {
const inspectedRequests = [];
server.on('request', (req, res) => {
const startTime = Date.now();
// Capture request data
const requestData = {
url: req.originalReq.url,
method: req.originalReq.method,
headers: req.originalReq.headers,
timestamp: startTime
};
// Capture response data
const originalEnd = res.end;
res.end = function(chunk, encoding) {
requestData.responseTime = Date.now() - startTime;
requestData.statusCode = res.statusCode;
inspectedRequests.push(requestData);
// Call original end method
originalEnd.call(this, chunk, encoding);
};
});
// Provide API to access captured data
server.on('upgrade', (req, socket, head) => {
if (req.url === '/ws/inspect') {
// WebSocket connection for real-time data
}
});
};
Advanced Features
Request/Response Modification
module.exports = (server, options) => {
return {
requestHandler: (req, res, next) => {
// Modify request headers
req.headers['x-custom-header'] = 'modified';
// Modify request body
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const data = JSON.parse(body);
data.modified = true;
const newBody = JSON.stringify(data);
req.headers['content-length'] = Buffer.byteLength(newBody);
// Continue with modified body
next();
} catch (e) {
next();
}
});
} else {
next();
}
},
responseHandler: (req, res, next) => {
// Modify response
const originalWrite = res.write;
const originalEnd = res.end;
res.write = function(chunk, encoding) {
// Modify response data
if (typeof chunk === 'string') {
chunk = chunk.replace(/error/g, 'success');
}
return originalWrite.call(this, chunk, encoding);
};
next();
}
};
};
Configuration Management
const fs = require('fs');
const path = require('path');
class ConfigManager {
constructor(configPath) {
this.configPath = configPath;
this.config = this.loadConfig();
}
loadConfig() {
try {
if (fs.existsSync(this.configPath)) {
return JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
}
} catch (e) {
console.error('Failed to load config:', e);
}
return {};
}
saveConfig() {
try {
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
} catch (e) {
console.error('Failed to save config:', e);
}
}
get(key) {
return this.config[key];
}
set(key, value) {
this.config[key] = value;
this.saveConfig();
}
}
module.exports = (server, options) => {
const configManager = new ConfigManager(
path.join(options.storage, 'plugin-config.json')
);
// Use configuration in plugin logic
return {
configManager,
// ... other handlers
};
};
Testing and Debugging
Unit Testing
// test/plugin.test.js
const assert = require('assert');
const plugin = require('../index');
describe('Plugin Tests', () => {
it('should handle requests correctly', (done) => {
const mockServer = {
on: (event, handler) => {
if (event === 'request') {
// Test request handling
const mockReq = { originalReq: { url: '/test' } };
const mockRes = { end: (data) => {
assert(data.includes('test'));
done();
}};
handler(mockReq, mockRes);
}
}
};
plugin(mockServer, {});
});
});
Debug Mode
const debug = require('debug')('whistle:plugin:yourname');
module.exports = (server, options) => {
debug('Plugin loaded with options:', options);
return {
requestHandler: (req, res, next) => {
debug('Processing request:', req.originalReq.url);
next();
}
};
};
Publishing and Distribution
NPM Publishing
# Prepare package
npm version patch
npm publish
# Install in whistle
npm install -g whistle.your-plugin
Local Development
# Link for development
npm link
cd whistle-project
npm link whistle.your-plugin
# Restart whistle to load plugin
w2 restart
Best Practices
1. Error Handling
module.exports = (server, options) => {
return {
requestHandler: (req, res, next) => {
try {
// Plugin logic
next();
} catch (error) {
console.error('Plugin error:', error);
next(); // Always call next to avoid hanging
}
}
};
};
2. Performance Optimization
// Use streams for large data
const stream = require('stream');
class ResponseTransform extends stream.Transform {
_transform(chunk, encoding, callback) {
// Transform response data efficiently
const transformed = chunk.toString().replace(/old/g, 'new');
callback(null, transformed);
}
}
3. Security Considerations
// Validate and sanitize inputs
function sanitizeInput(input) {
return input.replace(/[<>]/g, '');
}
// Limit resource usage
const requestCount = new Map();
function rateLimitCheck(ip) {
const count = requestCount.get(ip) || 0;
if (count > 100) {
return false; // Rate limited
}
requestCount.set(ip, count + 1);
return true;
}
Real-world Examples
Mock Server Plugin
module.exports = (server, options) => {
const mocks = new Map();
server.on('request', (req, res) => {
const { originalReq } = req;
const mockKey = `${originalReq.method}:${originalReq.url}`;
if (mocks.has(mockKey)) {
const mockResponse = mocks.get(mockKey);
res.writeHead(mockResponse.statusCode, mockResponse.headers);
res.end(mockResponse.body);
} else {
// Forward to actual server
req.passThrough();
}
});
// Management API
return {
addMock: (method, url, response) => {
mocks.set(`${method}:${url}`, response);
},
removeMock: (method, url) => {
mocks.delete(`${method}:${url}`);
}
};
};
Conclusion
Whistle plugin development provides powerful capabilities for network debugging and proxy customization. Key points to remember:
- Understand the Plugin Types: Choose the right type for your use case
- Follow Conventions: Use standard naming and structure
- Handle Errors Gracefully: Ensure plugins don’t break whistle
- Test Thoroughly: Test with various scenarios
- Document Well: Provide clear usage instructions
Start with simple plugins and gradually build more complex functionality as you become familiar with the whistle plugin system.
最近公司提供了云IDE服务,这样部分开发工作可以从本地切换到远程机器,同时现在前端也走向了微前端,开发时总是需要走代理。云IDE出现后,意味着代理的地址会随着IDE地址而变化,因此这里驱动出了,解决一键加载远程代理规则的需求。
云IDE出现后,整体开发流程如下
- 当任务管理系统下发一个story
- 开发者创建feat/fix分支,CI自动创建一台云IDE开发环境,同时创建出新的whistle代理规则文件
- CI发送消息告知云IDE地址及whistle代理规则文件地址
- 开发者点击whistle代理地址,直接加载该规则
- 开发者开始开发
如上的好处是,开发人员不用手动维护代理规则文件。
为了解决这样的需求,开始whistle插件开发,发现whistle提供了脚手架工具及一些demo,整体开发还是比较顺利的。但有些问题,官网并没有描述很清楚,这里简单总结下,兴许帮到些同行。
开发
大致实现逻辑如下
- 利用UI Server针对whistle web服务暴露新的URL,支持携带remoteRules参数
- 插件UI router中处理参数,根据参数值下载rule规则文件,临时保存
- 调用whistle命令
w2 add进行加载 - 重定向到whistle web rules页面下
插件安装及源码戳这里,兴许帮到些人
使用效果
$ npm i whistle -g
$ npm i whistle.remote-rules -g
# or
$ w2 i whistle.remote-rules
自动加载远程规则,地址模版如下
http://local.whistlejs.com/whistle.remote-rules/?remoteRules={url}
举个例子
http://local.whistlejs.com/whistle.remote-rules/?remoteRules=https%3A%2F%2Fgist.githubusercontent.com%2Falanhg%2Fa950d216121002d5bb0fd1278789da75%2Fraw%2Fc8bdf23e05c9dc504ea393a0b373cb556df911d8%2Ftest-whistle.js
有了该插件支持,CI方面就可以直接推送如上的地址,开发者点击加载后就可以继续开发调试了。
注意事项
whistle官方文档及案例有些方面描述的不够清晰,这里补充下
插件调试
# 如果whistle处于运行状态下,需要手动关闭 $ w2 stop # 运行whistle,这时是调试模式,插件脚本中的异常或者console打印信息均会在脚本启动的终端打印出来 $ w2 run插件开发,热更
# 脚本修改,实时编译,TS开发时需要 $ npm run dev # 监测dist及rules文件,如果有修复会自动更新加载的插件,如果更新,终端会打印reload信息 $ lack watch发布
插件发布,并不需要推送到whistle作者的repo或者whistle-plugins组织下,以个人名义发布即可,只需要注意,whistle约定包名称来确保被whistle识别,比如我开发的
whistle.remote-ruleswhistle-server
whistle提供了多种server,确定需求从而选择对应的server,比如需要拓展UI服务,实现自定义页面来执行某个动作,比如我这里的暴露URL,实现自动下载远程规则,所以依赖uiServer,而router对应就是自己实现的一些API或者子路由页面
写在最后
Whistle暴露的口子支持基于个人需求可以拓展代理feat,这点还是很赞的。

