cdp-tunnel 2.5.21 → 2.6.0
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/cli/index.js +258 -92
- package/extension-new/background.js +19 -1
- package/extension-new/cdp/handler/special.js +28 -28
- package/extension-new/core/debugger.js +35 -36
- package/extension-new/core/websocket.js +43 -37
- package/extension-new/manifest.json +3 -2
- package/extension-new/popup.html +79 -0
- package/extension-new/popup.js +114 -0
- package/extension-new/utils/config.js +13 -1
- package/package.json +6 -1
- package/server/proxy-server.js +321 -163
- package/server/saas/auth.js +128 -0
- package/server/saas/cdp-proxy.js +36 -0
- package/server/saas/db.js +48 -0
- package/server/saas/index.js +147 -0
- package/server/saas/routes.js +184 -0
- package/server/saas/web/index.html +803 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 认证工具 — JWT + API Key + 密码哈希
|
|
3
|
+
*/
|
|
4
|
+
const jwt = require('jsonwebtoken');
|
|
5
|
+
const bcrypt = require('bcryptjs');
|
|
6
|
+
const { v4: uuidv4 } = require('uuid');
|
|
7
|
+
const db = require('./db');
|
|
8
|
+
|
|
9
|
+
// JWT 密钥(生产环境应通过环境变量配置)
|
|
10
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'cdp-tunnel-saas-dev-secret-change-in-production';
|
|
11
|
+
const JWT_EXPIRES_IN = '24h';
|
|
12
|
+
|
|
13
|
+
// ====== 密码管理 ======
|
|
14
|
+
|
|
15
|
+
function hashPassword(password) {
|
|
16
|
+
return bcrypt.hashSync(password, 10);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function verifyPassword(password, hash) {
|
|
20
|
+
return bcrypt.compareSync(password, hash);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ====== JWT 管理 ======
|
|
24
|
+
|
|
25
|
+
function generateToken(user) {
|
|
26
|
+
return jwt.sign(
|
|
27
|
+
{ userId: user.id, email: user.email },
|
|
28
|
+
JWT_SECRET,
|
|
29
|
+
{ expiresIn: JWT_EXPIRES_IN }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function verifyToken(token) {
|
|
34
|
+
try {
|
|
35
|
+
return jwt.verify(token, JWT_SECRET);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ====== 用户管理 ======
|
|
42
|
+
|
|
43
|
+
function createUser(email, password, displayName) {
|
|
44
|
+
const id = uuidv4();
|
|
45
|
+
const passwordHash = hashPassword(password);
|
|
46
|
+
db.prepare(`
|
|
47
|
+
INSERT INTO users (id, email, password_hash, display_name)
|
|
48
|
+
VALUES (?, ?, ?, ?)
|
|
49
|
+
`).run(id, email, passwordHash, displayName || email.split('@')[0]);
|
|
50
|
+
return { id, email, displayName: displayName || email.split('@')[0] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findUserByEmail(email) {
|
|
54
|
+
return db.prepare('SELECT * FROM users WHERE email = ?').get(email);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findUserById(id) {
|
|
58
|
+
return db.prepare('SELECT id, email, display_name, created_at FROM users WHERE id = ?').get(id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function authenticateUser(email, password) {
|
|
62
|
+
const user = findUserByEmail(email);
|
|
63
|
+
if (!user) return null;
|
|
64
|
+
if (!verifyPassword(password, user.password_hash)) return null;
|
|
65
|
+
return { id: user.id, email: user.email, displayName: user.display_name };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ====== API Key 管理 ======
|
|
69
|
+
|
|
70
|
+
function generateApiKey() {
|
|
71
|
+
return 'cdp_' + uuidv4().replace(/-/g, '') + uuidv4().replace(/-/g, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createApiKey(userId, name) {
|
|
75
|
+
const id = uuidv4();
|
|
76
|
+
const key = generateApiKey();
|
|
77
|
+
db.prepare(`
|
|
78
|
+
INSERT INTO api_keys (id, user_id, key, name)
|
|
79
|
+
VALUES (?, ?, ?, ?)
|
|
80
|
+
`).run(id, userId, key, name || 'default');
|
|
81
|
+
return { id, key, name: name || 'default' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateApiKey(key) {
|
|
85
|
+
const apiKey = db.prepare('SELECT * FROM api_keys WHERE key = ? AND active = 1').get(key);
|
|
86
|
+
if (!apiKey) return null;
|
|
87
|
+
// 更新最后使用时间
|
|
88
|
+
db.prepare('UPDATE api_keys SET last_used_at = datetime("now") WHERE id = ?').run(apiKey.id);
|
|
89
|
+
return { userId: apiKey.user_id, keyId: apiKey.id, keyName: apiKey.name };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function listApiKeys(userId) {
|
|
93
|
+
return db.prepare('SELECT id, name, active, created_at, last_used_at FROM api_keys WHERE user_id = ?').all(userId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function revokeApiKey(keyId, userId) {
|
|
97
|
+
return db.prepare('UPDATE api_keys SET active = 0 WHERE id = ? AND user_id = ?').run(keyId, userId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ====== 初始化种子用户(仅开发环境) ======
|
|
101
|
+
|
|
102
|
+
function seedAdminUser() {
|
|
103
|
+
const existing = findUserByEmail('admin@cdp-tunnel.dev');
|
|
104
|
+
if (!existing) {
|
|
105
|
+
const user = createUser('admin@cdp-tunnel.dev', 'admin123', 'Admin');
|
|
106
|
+
const apiKey = createApiKey(user.id, 'default');
|
|
107
|
+
console.log('[SAAS] Seed admin user created');
|
|
108
|
+
console.log('[SAAS] Email: admin@cdp-tunnel.dev');
|
|
109
|
+
console.log('[SAAS] Password: admin123');
|
|
110
|
+
console.log('[SAAS] API Key:', apiKey.key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
hashPassword,
|
|
116
|
+
verifyPassword,
|
|
117
|
+
generateToken,
|
|
118
|
+
verifyToken,
|
|
119
|
+
createUser,
|
|
120
|
+
findUserByEmail,
|
|
121
|
+
findUserById,
|
|
122
|
+
authenticateUser,
|
|
123
|
+
createApiKey,
|
|
124
|
+
validateApiKey,
|
|
125
|
+
listApiKeys,
|
|
126
|
+
revokeApiKey,
|
|
127
|
+
seedAdminUser
|
|
128
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP 代理桥接
|
|
3
|
+
* SaaS API 服务器通过此模块查询 CDP 代理的浏览器信息
|
|
4
|
+
*/
|
|
5
|
+
const http = require('http');
|
|
6
|
+
|
|
7
|
+
const CDP_HOST = process.env.CDP_HOST || 'localhost';
|
|
8
|
+
const CDP_PORT = parseInt(process.env.CDP_PORT || '9221');
|
|
9
|
+
|
|
10
|
+
function cdpRequest(path) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
http.get(`http://${CDP_HOST}:${CDP_PORT}${path}`, (res) => {
|
|
13
|
+
let data = '';
|
|
14
|
+
res.on('data', chunk => data += chunk);
|
|
15
|
+
res.on('end', () => {
|
|
16
|
+
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
|
17
|
+
});
|
|
18
|
+
}).on('error', reject);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function getBrowsers(userId) {
|
|
23
|
+
try {
|
|
24
|
+
const browsers = await cdpRequest('/json/browsers');
|
|
25
|
+
if (!Array.isArray(browsers)) return [];
|
|
26
|
+
if (userId) {
|
|
27
|
+
return browsers.filter(b => b.userId === userId);
|
|
28
|
+
}
|
|
29
|
+
return browsers;
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error('[CDP] Failed to get browsers:', e.message);
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { getBrowsers, cdpRequest };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SaaS 多租户数据库
|
|
3
|
+
* SQLite — 单文件,零配置
|
|
4
|
+
*/
|
|
5
|
+
const Database = require('better-sqlite3');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
const DB_DIR = path.join(os.homedir(), '.cdp-tunnel');
|
|
11
|
+
const DB_PATH = path.join(DB_DIR, 'saas.db');
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(DB_DIR)) {
|
|
14
|
+
fs.mkdirSync(DB_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const db = new Database(DB_PATH);
|
|
18
|
+
|
|
19
|
+
// 启用 WAL 模式(高并发读友好)
|
|
20
|
+
db.pragma('journal_mode = WAL');
|
|
21
|
+
db.pragma('foreign_keys = ON');
|
|
22
|
+
|
|
23
|
+
// 创建表
|
|
24
|
+
db.exec(`
|
|
25
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
26
|
+
id TEXT PRIMARY KEY,
|
|
27
|
+
email TEXT UNIQUE NOT NULL,
|
|
28
|
+
password_hash TEXT NOT NULL,
|
|
29
|
+
display_name TEXT NOT NULL DEFAULT '',
|
|
30
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
31
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
37
|
+
key TEXT UNIQUE NOT NULL,
|
|
38
|
+
name TEXT NOT NULL DEFAULT 'default',
|
|
39
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
40
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
41
|
+
last_used_at TEXT
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
|
|
46
|
+
`);
|
|
47
|
+
|
|
48
|
+
module.exports = db;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Tunnel SaaS 平台入口
|
|
3
|
+
*
|
|
4
|
+
* 启动方式:
|
|
5
|
+
* node server/saas/index.js
|
|
6
|
+
*
|
|
7
|
+
* 环境变量:
|
|
8
|
+
* PORT=9220 API 服务器端口(默认 9220)
|
|
9
|
+
* CDP_PORT=9221 CDP 代理端口(默认 9221)
|
|
10
|
+
* JWT_SECRET JWT 签名密钥
|
|
11
|
+
*/
|
|
12
|
+
const http = require('http');
|
|
13
|
+
const httpProxy = require('http-proxy');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const { createRouter } = require('./routes');
|
|
17
|
+
const auth = require('./auth');
|
|
18
|
+
const { getBrowsers } = require('./cdp-proxy');
|
|
19
|
+
|
|
20
|
+
// 端口配置
|
|
21
|
+
const API_PORT = parseInt(process.env.PORT || '9220');
|
|
22
|
+
const CDP_PORT = parseInt(process.env.CDP_PORT || '9221');
|
|
23
|
+
const WEB_UI_DIR = path.join(__dirname, 'web');
|
|
24
|
+
|
|
25
|
+
// 安装 http-proxy 依赖
|
|
26
|
+
// npm install http-proxy
|
|
27
|
+
|
|
28
|
+
// 创建反向代理到 CDP 服务器
|
|
29
|
+
const cdpProxy = httpProxy.createProxyServer({
|
|
30
|
+
target: {
|
|
31
|
+
host: 'localhost',
|
|
32
|
+
port: CDP_PORT
|
|
33
|
+
},
|
|
34
|
+
ws: true // 支持 WebSocket 代理
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// CDP 路径列表(需要代理到 9221)
|
|
38
|
+
const CDP_PATHS = [
|
|
39
|
+
'/plugin',
|
|
40
|
+
'/client',
|
|
41
|
+
'/json',
|
|
42
|
+
'/devtools'
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Seed 管理员用户
|
|
46
|
+
auth.seedAdminUser();
|
|
47
|
+
|
|
48
|
+
// 创建路由
|
|
49
|
+
const pluginConnections = new Set();
|
|
50
|
+
const getNamespace = () => ({});
|
|
51
|
+
const router = createRouter(pluginConnections, getNamespace);
|
|
52
|
+
|
|
53
|
+
const server = http.createServer((req, res) => {
|
|
54
|
+
// CORS
|
|
55
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
56
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
57
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
58
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
59
|
+
|
|
60
|
+
if (req.method === 'OPTIONS') {
|
|
61
|
+
res.writeHead(204);
|
|
62
|
+
res.end();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const url = new URL(req.url, `http://localhost`);
|
|
67
|
+
|
|
68
|
+
// CDP 路径 → 代理到 9221
|
|
69
|
+
const shouldProxy = CDP_PATHS.some(p => url.pathname === p || url.pathname.startsWith(p + '/'));
|
|
70
|
+
if (shouldProxy) {
|
|
71
|
+
cdpProxy.web(req, res, { target: { host: 'localhost', port: CDP_PORT } }, (err) => {
|
|
72
|
+
console.error('[PROXY] CDP proxy error:', err.message);
|
|
73
|
+
res.writeHead(502);
|
|
74
|
+
res.end('Bad Gateway');
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// API: 浏览器列表 — 从 CDP 代理获取
|
|
80
|
+
if (req.method === 'GET' && url.pathname === '/api/browsers') {
|
|
81
|
+
const authHeader = req.headers['authorization'];
|
|
82
|
+
if (!authHeader) {
|
|
83
|
+
res.writeHead(401);
|
|
84
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const token = authHeader.replace('Bearer ', '');
|
|
88
|
+
const session = auth.verifyToken(token);
|
|
89
|
+
if (!session) {
|
|
90
|
+
res.writeHead(401);
|
|
91
|
+
res.end(JSON.stringify({ error: 'Invalid token' }));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getBrowsers(session.userId).then(browsers => {
|
|
96
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
97
|
+
res.end(JSON.stringify({ browsers }));
|
|
98
|
+
}).catch(e => {
|
|
99
|
+
res.writeHead(500);
|
|
100
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 静态文件(Web UI)
|
|
106
|
+
if (req.method === 'GET') {
|
|
107
|
+
let filePath;
|
|
108
|
+
if (url.pathname === '/' || url.pathname === '/index.html') {
|
|
109
|
+
filePath = path.join(WEB_UI_DIR, 'index.html');
|
|
110
|
+
} else {
|
|
111
|
+
filePath = path.join(WEB_UI_DIR, url.pathname);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
115
|
+
const extMap = {
|
|
116
|
+
'.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css',
|
|
117
|
+
'.png': 'image/png', '.svg': 'image/svg+xml', '.ico': 'image/x-icon',
|
|
118
|
+
'.json': 'application/json'
|
|
119
|
+
};
|
|
120
|
+
const ext = path.extname(filePath);
|
|
121
|
+
res.writeHead(200, { 'Content-Type': extMap[ext] || 'text/plain' });
|
|
122
|
+
res.end(fs.readFileSync(filePath));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 其他 API 路由
|
|
128
|
+
router(req, res);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// WebSocket 升级处理 — CDP 路径代理到 9221
|
|
132
|
+
server.on('upgrade', (req, socket, head) => {
|
|
133
|
+
const url = new URL(req.url, `http://localhost`);
|
|
134
|
+
const shouldProxy = CDP_PATHS.some(p => url.pathname === p || url.pathname.startsWith(p + '/'));
|
|
135
|
+
if (shouldProxy) {
|
|
136
|
+
cdpProxy.ws(req, socket, head, { target: { host: 'localhost', port: CDP_PORT } });
|
|
137
|
+
} else {
|
|
138
|
+
socket.destroy();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
server.listen(API_PORT, '0.0.0.0', () => {
|
|
143
|
+
console.log(`[SAAS] Server started on port ${API_PORT}`);
|
|
144
|
+
console.log(`[SAAS] CDP proxy: http://localhost:${CDP_PORT}`);
|
|
145
|
+
console.log(`[SAAS] Web UI: http://localhost:${API_PORT}`);
|
|
146
|
+
console.log(`[SAAS] Login: admin@cdp-tunnel.dev / admin123`);
|
|
147
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SaaS REST API 路由
|
|
3
|
+
*/
|
|
4
|
+
const { v4: uuidv4 } = require('uuid');
|
|
5
|
+
const auth = require('./auth');
|
|
6
|
+
|
|
7
|
+
function createRouter(pluginConnections, getNamespace) {
|
|
8
|
+
// 返回 express 风格的 router 函数:fn(req, res)
|
|
9
|
+
const routes = [];
|
|
10
|
+
|
|
11
|
+
function get(method, path, handler) {
|
|
12
|
+
routes.push({ method, path, handler });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function post(method, path, handler) {
|
|
16
|
+
routes.push({ method: 'POST', path, handler });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseBody(req) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
let body = '';
|
|
22
|
+
req.on('data', chunk => body += chunk);
|
|
23
|
+
req.on('end', () => {
|
|
24
|
+
try { resolve(JSON.parse(body)); } catch { resolve({}); }
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function json(res, data, status = 200) {
|
|
30
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
31
|
+
res.end(JSON.stringify(data));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function error(res, message, status = 400) {
|
|
35
|
+
json(res, { error: message }, status);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ====== 认证中间件 ======
|
|
39
|
+
function requireAuth(req) {
|
|
40
|
+
const authHeader = req.headers['authorization'];
|
|
41
|
+
if (!authHeader) return null;
|
|
42
|
+
const token = authHeader.replace('Bearer ', '');
|
|
43
|
+
return auth.verifyToken(token);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ====== Auth Routes ======
|
|
47
|
+
|
|
48
|
+
post('POST', '/api/auth/login', async (req, res) => {
|
|
49
|
+
const body = await parseBody(req);
|
|
50
|
+
const { email, password } = body;
|
|
51
|
+
if (!email || !password) return error(res, 'Email and password required');
|
|
52
|
+
|
|
53
|
+
const user = auth.authenticateUser(email, password);
|
|
54
|
+
if (!user) return error(res, 'Invalid email or password', 401);
|
|
55
|
+
|
|
56
|
+
const token = auth.generateToken(user);
|
|
57
|
+
const apiKeys = auth.listApiKeys(user.id);
|
|
58
|
+
|
|
59
|
+
json(res, {
|
|
60
|
+
token,
|
|
61
|
+
user: {
|
|
62
|
+
id: user.id,
|
|
63
|
+
email: user.email,
|
|
64
|
+
displayName: user.displayName
|
|
65
|
+
},
|
|
66
|
+
apiKeys
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
get('GET', '/api/auth/me', async (req, res) => {
|
|
71
|
+
const session = requireAuth(req);
|
|
72
|
+
if (!session) return error(res, 'Unauthorized', 401);
|
|
73
|
+
|
|
74
|
+
const user = auth.findUserById(session.userId);
|
|
75
|
+
if (!user) return error(res, 'User not found', 404);
|
|
76
|
+
|
|
77
|
+
json(res, { user });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ====== API Key Routes ======
|
|
81
|
+
|
|
82
|
+
get('GET', '/api/api-keys', async (req, res) => {
|
|
83
|
+
const session = requireAuth(req);
|
|
84
|
+
if (!session) return error(res, 'Unauthorized', 401);
|
|
85
|
+
|
|
86
|
+
const keys = auth.listApiKeys(session.userId);
|
|
87
|
+
json(res, { apiKeys: keys });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
post('POST', '/api/api-keys', async (req, res) => {
|
|
91
|
+
const session = requireAuth(req);
|
|
92
|
+
if (!session) return error(res, 'Unauthorized', 401);
|
|
93
|
+
|
|
94
|
+
const body = await parseBody(req);
|
|
95
|
+
const apiKey = auth.createApiKey(session.userId, body.name);
|
|
96
|
+
json(res, { apiKey }, 201);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
post('POST', '/api/api-keys/:id/revoke', async (req, res) => {
|
|
100
|
+
const session = requireAuth(req);
|
|
101
|
+
if (!session) return error(res, 'Unauthorized', 401);
|
|
102
|
+
|
|
103
|
+
// 从 URL 中提取 id
|
|
104
|
+
const id = req.url.split('/').filter(Boolean).pop();
|
|
105
|
+
const result = auth.revokeApiKey(id, session.userId);
|
|
106
|
+
if (result.changes === 0) return error(res, 'API key not found', 404);
|
|
107
|
+
json(res, { success: true });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ====== Browser Routes ======
|
|
111
|
+
|
|
112
|
+
get('GET', '/api/browsers', async (req, res) => {
|
|
113
|
+
const session = requireAuth(req);
|
|
114
|
+
if (!session) return error(res, 'Unauthorized', 401);
|
|
115
|
+
|
|
116
|
+
const browsers = [];
|
|
117
|
+
for (const pluginWs of pluginConnections) {
|
|
118
|
+
if (pluginWs.readyState !== WebSocket.OPEN) continue;
|
|
119
|
+
// 只返回属于当前用户的 plugin
|
|
120
|
+
if (pluginWs.userId !== session.userId) continue;
|
|
121
|
+
|
|
122
|
+
const ns = getNamespace(pluginWs);
|
|
123
|
+
browsers.push({
|
|
124
|
+
pluginId: pluginWs.pluginId,
|
|
125
|
+
name: pluginWs.pluginName || 'My Browser',
|
|
126
|
+
targets: ns.cachedTargets.length,
|
|
127
|
+
connected: true,
|
|
128
|
+
connectedAt: pluginWs.connectedAt,
|
|
129
|
+
webSocketDebuggerUrl: `ws://${req.headers.host}/devtools/browser/${pluginWs.pluginId}`,
|
|
130
|
+
// 给 web UI 用的直接连接地址
|
|
131
|
+
cdpHttpUrl: `https://${req.headers.host}/json/version/${pluginWs.pluginId}`
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
json(res, { browsers });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ====== 返回路由处理函数 ======
|
|
139
|
+
|
|
140
|
+
return async (req, res) => {
|
|
141
|
+
// CORS
|
|
142
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
143
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
144
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
145
|
+
|
|
146
|
+
if (req.method === 'OPTIONS') {
|
|
147
|
+
res.writeHead(204);
|
|
148
|
+
res.end();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const url = new URL(req.url, `http://localhost`);
|
|
153
|
+
|
|
154
|
+
for (const route of routes) {
|
|
155
|
+
if (route.method !== req.method) continue;
|
|
156
|
+
|
|
157
|
+
// 简单的路径匹配(支持 :id 占位符)
|
|
158
|
+
const routeParts = route.path.split('/').filter(Boolean);
|
|
159
|
+
const urlParts = url.pathname.split('/').filter(Boolean);
|
|
160
|
+
|
|
161
|
+
if (routeParts.length !== urlParts.length) continue;
|
|
162
|
+
|
|
163
|
+
let match = true;
|
|
164
|
+
const params = {};
|
|
165
|
+
for (let i = 0; i < routeParts.length; i++) {
|
|
166
|
+
if (routeParts[i].startsWith(':')) {
|
|
167
|
+
params[routeParts[i].slice(1)] = urlParts[i];
|
|
168
|
+
} else if (routeParts[i] !== urlParts[i]) {
|
|
169
|
+
match = false;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (match) {
|
|
175
|
+
req.params = params;
|
|
176
|
+
return route.handler(req, res);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
json(res, { error: 'Not found' }, 404);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { createRouter };
|