whistle.pastekitlab 1.6.9 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +224 -31
- package/package.json +2 -1
package/index.js
CHANGED
|
@@ -1,43 +1,236 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Whistle Plugin for PasteKit Lab(最终版 - 只监听)
|
|
3
|
+
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const WebSocket = require('ws');
|
|
6
|
+
const axios = require('axios');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// ==================== 配置 ====================
|
|
11
|
+
const CONFIG = {
|
|
12
|
+
wsPort: 8889,
|
|
13
|
+
logEnabled: true,
|
|
14
|
+
logFile: path.join(__dirname, 'plugin.log')
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ==================== 全局状态 ====================
|
|
18
|
+
let wss = null;
|
|
19
|
+
let clients = new Map();
|
|
20
|
+
let clientIdCounter = 0;
|
|
21
|
+
let domainConfig = [];
|
|
22
|
+
|
|
23
|
+
// ==================== 日志 ====================
|
|
24
|
+
function log(message) {
|
|
25
|
+
if (!CONFIG.logEnabled) return;
|
|
26
|
+
|
|
27
|
+
const timestamp = new Date().toISOString();
|
|
28
|
+
const logMessage = `[${timestamp}] ${message}\n`;
|
|
29
|
+
|
|
30
|
+
console.log('[pastekitlab]', message);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
fs.appendFileSync(CONFIG.logFile, logMessage, 'utf8');
|
|
34
|
+
} catch (e) {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ==================== 工具 ====================
|
|
39
|
+
function bufferToString(buffer) {
|
|
40
|
+
try {
|
|
41
|
+
if (!buffer) return null;
|
|
42
|
+
const result = buffer.toString('utf8');
|
|
43
|
+
if (/[^\x20-\x7E\r\n\t]/.test(result)) return null;
|
|
44
|
+
return result;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
11
49
|
|
|
12
|
-
|
|
13
|
-
|
|
50
|
+
function bufferToBase64(buffer) {
|
|
51
|
+
try {
|
|
52
|
+
return buffer ? buffer.toString('base64') : null;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
14
57
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// 这里再调用 broadcast(...)
|
|
19
|
-
}).catch(e => console.error('❌ getBody error:', e.message));
|
|
58
|
+
function generateId() {
|
|
59
|
+
return 'req_' + Date.now() + '_' + Math.random().toString(36).slice(2);
|
|
60
|
+
}
|
|
20
61
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
62
|
+
// ==================== 域名过滤 ====================
|
|
63
|
+
function shouldIntercept(url) {
|
|
64
|
+
if (!domainConfig.length) return true;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const {hostname} = new URL(url);
|
|
68
|
+
|
|
69
|
+
return domainConfig.some(domain => {
|
|
70
|
+
if (hostname === domain) return true;
|
|
71
|
+
if (domain.startsWith('*.')) {
|
|
72
|
+
const base = domain.slice(2);
|
|
73
|
+
return hostname.endsWith('.' + base) || hostname === base;
|
|
24
74
|
}
|
|
25
|
-
|
|
75
|
+
return false;
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ==================== WebSocket ====================
|
|
83
|
+
function startWebSocketServer() {
|
|
84
|
+
if (wss) return;
|
|
85
|
+
|
|
86
|
+
wss = new WebSocket.Server({
|
|
87
|
+
port: CONFIG.wsPort,
|
|
88
|
+
path: '/ws'
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
wss.on('connection', (ws, req) => {
|
|
92
|
+
const id = ++clientIdCounter;
|
|
26
93
|
|
|
27
|
-
|
|
94
|
+
clients.set(id, ws);
|
|
95
|
+
log(`客户端连接: ${id}`);
|
|
96
|
+
|
|
97
|
+
ws.send(JSON.stringify({
|
|
98
|
+
msgType: 'connected',
|
|
99
|
+
clientId: id
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
ws.on('message', (data) => {
|
|
28
103
|
try {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
104
|
+
const msg = JSON.parse(data.toString());
|
|
105
|
+
|
|
106
|
+
if (msg.msgType === 'domain') {
|
|
107
|
+
domainConfig = msg.domain || [];
|
|
108
|
+
log(`更新域名配置: ${domainConfig.join(',')}`);
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
});
|
|
32
113
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
114
|
+
ws.on('close', () => {
|
|
115
|
+
clients.delete(id);
|
|
116
|
+
log(`客户端断开: ${id}`);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
36
119
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
120
|
+
log(`WebSocket 启动: ws://127.0.0.1:${CONFIG.wsPort}/ws`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function broadcast(type, data) {
|
|
124
|
+
if (!clients.size) return;
|
|
125
|
+
|
|
126
|
+
const msg = JSON.stringify({
|
|
127
|
+
msgType: type,
|
|
128
|
+
...data,
|
|
129
|
+
timestamp: Date.now()
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
clients.forEach(ws => {
|
|
133
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
134
|
+
ws.send(msg);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ==================== Whistle 插件主逻辑 ====================
|
|
140
|
+
module.exports = (server, options) => {
|
|
141
|
+
log('插件已启动(使用 axios 转发请求)');
|
|
142
|
+
startWebSocketServer();
|
|
143
|
+
|
|
144
|
+
server.on('request', async (req, res) => {
|
|
145
|
+
try {
|
|
146
|
+
const url = req.fullUrl || req.url;
|
|
147
|
+
console.log('>>> [GLOBAL]', url);
|
|
148
|
+
|
|
149
|
+
// 收集请求体
|
|
150
|
+
let reqBody = '';
|
|
151
|
+
for await (const chunk of req) {
|
|
152
|
+
reqBody += chunk;
|
|
40
153
|
}
|
|
154
|
+
|
|
155
|
+
// 构建请求数据
|
|
156
|
+
const requestData = {
|
|
157
|
+
eventId: generateId(),
|
|
158
|
+
url,
|
|
159
|
+
method: req.method,
|
|
160
|
+
headers: {...req.headers},
|
|
161
|
+
body: bufferToString(Buffer.from(reqBody)),
|
|
162
|
+
bodyBase64: Buffer.from(reqBody).toString('base64'),
|
|
163
|
+
timestamp: Date.now()
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// 通知 Chrome 插件:请求开始
|
|
167
|
+
broadcast('REQUEST', requestData);
|
|
168
|
+
log(`REQ ${req.method} ${url}`);
|
|
169
|
+
|
|
170
|
+
// 通过 axios 转发请求
|
|
171
|
+
const startTime = Date.now();
|
|
172
|
+
const response = await axios({
|
|
173
|
+
method: req.method.toLowerCase(),
|
|
174
|
+
url: url,
|
|
175
|
+
headers: req.headers,
|
|
176
|
+
data: reqBody || undefined,
|
|
177
|
+
validateStatus: () => true // 不抛出 HTTP 错误
|
|
178
|
+
});
|
|
179
|
+
const duration = Date.now() - startTime;
|
|
180
|
+
|
|
181
|
+
// 设置响应状态码和头
|
|
182
|
+
res.statusCode = response.status;
|
|
183
|
+
Object.keys(response.headers).forEach(key => {
|
|
184
|
+
res.setHeader(key, response.headers[key]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// 返回响应体
|
|
188
|
+
res.end(response.data);
|
|
189
|
+
|
|
190
|
+
// 构建响应数据
|
|
191
|
+
const responseData = {
|
|
192
|
+
...requestData,
|
|
193
|
+
statusCode: response.status,
|
|
194
|
+
responseHeaders: {...response.headers},
|
|
195
|
+
responseBody: bufferToString(
|
|
196
|
+
typeof response.data === 'string'
|
|
197
|
+
? Buffer.from(response.data)
|
|
198
|
+
: Buffer.from(JSON.stringify(response.data))
|
|
199
|
+
),
|
|
200
|
+
responseBodyBase64: (
|
|
201
|
+
typeof response.data === 'string'
|
|
202
|
+
? Buffer.from(response.data)
|
|
203
|
+
: Buffer.from(JSON.stringify(response.data))
|
|
204
|
+
).toString('base64'),
|
|
205
|
+
duration
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// 通知 Chrome 插件:响应完成
|
|
209
|
+
broadcast('RESPONSE', responseData);
|
|
210
|
+
log(`转发成功: ${req.method} ${url} -> ${response.status} (${duration}ms)`);
|
|
211
|
+
|
|
212
|
+
} catch (error) {
|
|
213
|
+
log(`转发失败: ${error.message}`);
|
|
214
|
+
res.statusCode = 500;
|
|
215
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
216
|
+
|
|
217
|
+
// 通知 Chrome 插件:请求失败
|
|
218
|
+
broadcast('ERROR', {
|
|
219
|
+
url: req.fullUrl || req.url,
|
|
220
|
+
method: req.method,
|
|
221
|
+
error: error.message,
|
|
222
|
+
timestamp: Date.now()
|
|
223
|
+
});
|
|
41
224
|
}
|
|
42
|
-
};
|
|
225
|
+
});
|
|
43
226
|
};
|
|
227
|
+
|
|
228
|
+
// UI 接口保持不变
|
|
229
|
+
module.exports.ui = (uiServer, options) => {
|
|
230
|
+
uiServer.on('request', (req, res) => {
|
|
231
|
+
if (req.url === '/plugin/pastekitlab/config') {
|
|
232
|
+
res.setHeader('Content-Type', 'application/json');
|
|
233
|
+
res.end(JSON.stringify({ status: 'ok', wsPort: CONFIG.wsPort }));
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whistle.pastekitlab",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "Whistle plugin for PasteKit Lab - Intercepts requests and sends them to requestlistviewer via WebSocket",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"author": "PasteKit Lab",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"axios": "^1.6.0",
|
|
19
20
|
"ws": "^8.19.0"
|
|
20
21
|
},
|
|
21
22
|
"whistlePlugin": true,
|