Skip to content

Commit c85d8cb

Browse files
committed
security(cli): harden auth and execution paths
1 parent c0b491e commit c85d8cb

11 files changed

Lines changed: 901 additions & 448 deletions

File tree

packages/cli/src/auth/feishuAuth.ts

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
* SPDX-License-Identifier: Apache-2.0
66
*/
77

8-
98
import * as crypto from 'crypto';
109
import * as http from 'http';
1110
import { URL } from 'url';
12-
import { exec } from 'child_process';
11+
import open from 'open';
1312
import { appEvents, AppEvent } from '../utils/events.js';
1413

15-
1614
// 功能实现: 飞书OAuth2认证集成
1715
// 实现方案: 基于飞书开放平台OAuth2授权码模式
1816
// 影响范围: 新增认证模块,集成到现有认证流程
@@ -59,10 +57,10 @@ export class FeishuAuthHandler {
5957
*/
6058
public buildAuthUrl(): string {
6159
const params = new URLSearchParams({
62-
app_id: this.config.appId, // 飞书使用app_id参数
60+
app_id: this.config.appId, // 飞书使用app_id参数
6361
redirect_uri: this.config.redirectUri,
6462
response_type: 'code',
65-
scope: 'contact:user.employee_id:readonly', // 使用正确的scope
63+
scope: 'contact:user.employee_id:readonly', // 使用正确的scope
6664
state: this.state,
6765
});
6866

@@ -111,7 +109,10 @@ export class FeishuAuthHandler {
111109

112110
// 如果端口改变了,需要更新配置
113111
if (currentPort !== port) {
114-
const newRedirectUri = this.config.redirectUri.replace(`:${port}`, `:${currentPort}`);
112+
const newRedirectUri = this.config.redirectUri.replace(
113+
`:${port}`,
114+
`:${currentPort}`,
115+
);
115116
this.config.redirectUri = newRedirectUri;
116117
console.log(`📝 重定向URI已更新为: ${newRedirectUri}`);
117118
}
@@ -122,21 +123,30 @@ export class FeishuAuthHandler {
122123
console.log(`🚀 正在打开浏览器进行飞书授权...`);
123124

124125
// 自动打开浏览器
125-
this.openBrowser(authUrl);
126+
void this.openBrowser(authUrl);
126127
});
127128

128129
this.server!.on('error', (err: any) => {
129130
if (err.code === 'EADDRINUSE') {
130-
console.log(`⚠️ 端口 ${currentPort} 被占用,尝试端口 ${currentPort + 1}`);
131-
if (currentPort < 6709) { // 最多尝试10个端口 (6699-6709)
131+
console.log(
132+
`⚠️ 端口 ${currentPort} 被占用,尝试端口 ${currentPort + 1}`,
133+
);
134+
if (currentPort < 6709) {
135+
// 最多尝试10个端口 (6699-6709)
132136
tryListen(currentPort + 1);
133137
} else {
134138
this.cleanup();
135-
resolve({ success: false, error: '无法找到可用端口 (6699-6709)' });
139+
resolve({
140+
success: false,
141+
error: '无法找到可用端口 (6699-6709)',
142+
});
136143
}
137144
} else {
138145
this.cleanup();
139-
resolve({ success: false, error: `服务器启动失败: ${err.message}` });
146+
resolve({
147+
success: false,
148+
error: `服务器启动失败: ${err.message}`,
149+
});
140150
}
141151
});
142152
};
@@ -154,27 +164,16 @@ export class FeishuAuthHandler {
154164
/**
155165
* 自动打开浏览器到指定URL
156166
*/
157-
private openBrowser(url: string): void {
158-
const platform = process.platform;
159-
160-
let command: string;
161-
if (platform === 'win32') {
162-
command = `start "" "${url}"`;
163-
} else if (platform === 'darwin') {
164-
command = `open "${url}"`;
165-
} else {
166-
command = `xdg-open "${url}"`;
167+
private async openBrowser(url: string): Promise<void> {
168+
try {
169+
await open(url, { wait: false });
170+
console.log('✅ 浏览器已打开,请在飞书页面完成授权');
171+
} catch (error) {
172+
const message = error instanceof Error ? error.message : String(error);
173+
console.error(`❌ 无法自动打开浏览器: ${message}`);
174+
console.log('📋 请手动复制以下URL到浏览器中打开:');
175+
console.log(`🔗 ${url}`);
167176
}
168-
169-
exec(command, (error: any) => {
170-
if (error) {
171-
console.error(`❌ 无法自动打开浏览器: ${error.message}`);
172-
console.log(`📋 请手动复制以下URL到浏览器中打开:`);
173-
console.log(`🔗 ${url}`);
174-
} else {
175-
console.log(`✅ 浏览器已打开,请在飞书页面完成授权`);
176-
}
177-
});
178177
}
179178

180179
/**
@@ -184,7 +183,7 @@ export class FeishuAuthHandler {
184183
private async handleCallbackWithPlatCheck(
185184
reqUrl: URL,
186185
res: http.ServerResponse,
187-
resolve: (result: FeishuAuthResult) => void
186+
resolve: (result: FeishuAuthResult) => void,
188187
): Promise<void> {
189188
// 直接处理飞书认证回调,不再处理DeepVlab
190189
console.log('🔄 [FeishuAuth] 处理飞书认证回调(旧流程)');
@@ -197,7 +196,7 @@ export class FeishuAuthHandler {
197196
private async handleCallback(
198197
reqUrl: URL,
199198
res: http.ServerResponse,
200-
resolve: (result: FeishuAuthResult) => void
199+
resolve: (result: FeishuAuthResult) => void,
201200
): Promise<void> {
202201
const code = reqUrl.searchParams.get('code');
203202
const state = reqUrl.searchParams.get('state');
@@ -228,10 +227,10 @@ export class FeishuAuthHandler {
228227
const accessToken = await this.exchangeCodeForToken(code);
229228
this.sendSuccessResponse(res);
230229
this.cleanup();
231-
resolve({
232-
success: true,
230+
resolve({
231+
success: true,
233232
accessToken,
234-
nextStepUrl: this.config.nextStepUrl
233+
nextStepUrl: this.config.nextStepUrl,
235234
});
236235
} catch (error) {
237236
const errorMsg = error instanceof Error ? error.message : '未知错误';
@@ -256,16 +255,15 @@ export class FeishuAuthHandler {
256255
// 修复策略: 回到标准OAuth2规范,移除重复的body构建逻辑
257256
// 影响范围: packages/cli/src/auth/feishuAuth.ts:231-262
258257
// 修复日期: 2025-01-26
259-
258+
260259
const formData = new URLSearchParams({
261260
grant_type: 'authorization_code',
262-
client_id: this.config.appId, // 使用标准OAuth2参数名
261+
client_id: this.config.appId, // 使用标准OAuth2参数名
263262
client_secret: this.config.appSecret, // 使用标准OAuth2参数名
264263
code: code,
265264
redirect_uri: this.config.redirectUri,
266265
});
267266

268-
269267
const response = await fetch(tokenUrl, {
270268
method: 'POST',
271269
headers: {
@@ -282,7 +280,9 @@ export class FeishuAuthHandler {
282280
if (!response.ok) {
283281
const errorText = await response.text();
284282
console.error('❌ exchangeCodeForToken: 错误响应内容:', errorText);
285-
throw new Error(`HTTP ${response.status}: ${response.statusText}\n响应内容: ${errorText}`);
283+
throw new Error(
284+
`HTTP ${response.status}: ${response.statusText}\n响应内容: ${errorText}`,
285+
);
286286
}
287287

288288
const data = await response.json();
@@ -293,17 +293,19 @@ export class FeishuAuthHandler {
293293
// 修复策略: OAuth2标准响应通常直接包含access_token,而不是通过code字段判断
294294
// 影响范围: packages/cli/src/auth/feishuAuth.ts:276-280
295295
// 修复日期: 2025-01-26
296-
296+
297297
// 检查OAuth2标准错误格式
298298
if (data.error) {
299-
throw new Error(`飞书OAuth2错误: ${data.error} - ${data.error_description || ''}`);
299+
throw new Error(
300+
`飞书OAuth2错误: ${data.error} - ${data.error_description || ''}`,
301+
);
300302
}
301-
303+
302304
// 检查飞书特有的code字段(如果存在)
303305
if (data.code !== undefined && data.code !== 0) {
304306
throw new Error(`飞书API错误: ${data.msg || data.error || '未知错误'}`);
305307
}
306-
308+
307309
// 检查是否有access_token
308310
if (!data.access_token) {
309311
throw new Error('响应中缺少access_token字段');
@@ -399,13 +401,11 @@ export class FeishuAuthHandler {
399401
</body>
400402
</html>
401403
`;
402-
404+
403405
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
404406
res.end(html);
405407
}
406408

407-
408-
409409
/**
410410
* 发送错误响应
411411
* 功能实现: 飞书认证失败后显示友好的错误页面并自动关闭
@@ -477,7 +477,7 @@ export class FeishuAuthHandler {
477477
</body>
478478
</html>
479479
`;
480-
480+
481481
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
482482
res.end(html);
483483
}
@@ -501,13 +501,16 @@ export class FeishuAuthHandler {
501501
export function createFeishuAuthHandler(
502502
appId: string,
503503
appSecret: string,
504-
nextStepUrl?: string
504+
nextStepUrl?: string,
505505
): FeishuAuthHandler {
506506
const config: FeishuAuthConfig = {
507507
appId,
508508
appSecret,
509-
redirectUri: 'http://localhost:7863/callback', // 使用与飞书应用配置匹配的回调地址
510-
nextStepUrl: nextStepUrl || process.env.DEEPX_SERVER_URL || 'https://api-code.deepvlab.ai',
509+
redirectUri: 'http://localhost:7863/callback', // 使用与飞书应用配置匹配的回调地址
510+
nextStepUrl:
511+
nextStepUrl ||
512+
process.env.DEEPX_SERVER_URL ||
513+
'https://api-code.deepvlab.ai',
511514
};
512515

513516
return new FeishuAuthHandler(config);

0 commit comments

Comments
 (0)