chore: 批量代码优化与功能迭代更新

本次提交包含大量代码优化、功能新增与服务端配置更新:
1. 修复分析报告统计数据,调整CMake策略设置
2. 优化APP权限配置、编辑器与聊天界面组件
3. 更新依赖库版本与pubspec配置
4. 新增文件传输服务端、信令服务器相关配置与脚本
5. 完善用户注销功能与数据库迁移脚本
6. 优化多处动画效果、代码风格与日志输出
7. 新增多种调试与部署脚本,修复已知BUG
This commit is contained in:
Developer
2026-05-12 06:28:04 +08:00
parent 72f64f9ca9
commit 283950ea07
245 changed files with 50255 additions and 6160 deletions

View File

@@ -0,0 +1,60 @@
# 文件传输功能验收审计报告
> 审计时间: 2026-05-11T02:00:32.006752
> 审计范围: 阶段一~六 共34项任务 (file-transfer-dev-plan.md L544-L620)
## 审计结论总览
| 指标 | 数量 | 占比 |
|------|------|------|
| ✅ 通过 | 27 | 79% |
| 🔴 失败 | 2 | 6% |
| 🟡 警告 | 5 | 15% |
## 严重问题清单 (🔴 需要修复)
### [2.6] BLE广播实现
- **问题**: ⚠️ 检测到桩代码! startAdvertising方法是空实现 — 仅打日志不实际广播。flutter_blue_plus确实不暴露此API,需要平台Channel或nearby_service替代方案。标记"完成(占位)"虽然诚实,但实际功能不可用。
- **风险等级**: 高
- **建议**: 需要补全实现
### [5.8] 传输路由自动降级
- **问题**: ⚠️ 严重: sendWithFallback 方法不存在! 任务声称已完成但实际上只实现了selectRoute路由选择,没有实现带自动降级的sendWithFallback方法。当前路由选择后由provider手动分发到对应service,没有fallback重试机制。
- **风险等级**: 高
- **建议**: 需要补全实现
## 改进建议清单 (🟡 建议优化)
### [3.4] 传输暂停/恢复/取消
- **建议**: 部分传输方式未完全实现pause/resume/cancel
### [4.2] TURN服务器部署脚本
- **建议**: deploy_coturn.py 未找到(可能在服务器上)
### [4.3] WebSocket信令服务器
- **建议**: server.js 未在本地找到
### [4.4] 配对数据持久化
- **建议**: PHP后端文件未找到(应在服务器)
### [4.6] API文档
- **建议**: API文档未找到
## 各阶段通过率
| 阶段一 P0 紧急修复 | 3/3 | 100% |
| 阶段二 P1 核心功能 | 10/11 | 91% 🔴1 |
| 阶段三 P2 质量稳定性 | 4/5 | 80% 🟡1 |
| 阶段四 服务端 | 1/5 | 20% 🟡4 |
| 阶段五 扩展功能 | 7/8 | 88% 🔴1 |
| 阶段六 DTO | 1/1 | 100% |
---
*报告由自动化审计脚本生成, 基于静态代码分析*

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,810 @@
// ============================================================
// 闲言APP — 文件传输深度逻辑审计脚本 (第二阶段)
// 创建时间: 2026-05-11
// 更新时间: 2026-05-11
// 作用: 深度检测代码逻辑缺陷 — 空实现/桩代码/私有字段访问/
// 资源泄漏/错误处理缺失/协议不完整/安全漏洞
//
// 审计维度:
// 🔴 CRITICAL — 功能性缺陷,影响核心功能可用性
// 🟠 HIGH — 严重代码质量问题,可能导致运行时崩溃
// 🟡 MEDIUM — 代码规范问题,影响可维护性
// ============================================================
import 'dart:io';
final _deepResults = <_DeepAuditResult>[];
int _criticalCount = 0;
int _highCount = 0;
int _mediumCount = 0;
void main() async {
print('╔══════════════════════════════════════════════════════════════╗');
print('║ 闲言APP — 文件传输深度逻辑审计 (第二阶段) ║');
print('║ 审计维度: 代码逻辑/空实现/资源泄漏/安全漏洞 ║');
print('${DateTime.now().toIso8601String()}');
print('╚══════════════════════════════════════════════════════════════╝\n');
await _section('A. 空实现与桩代码检测');
await _auditStubImplementations();
await _section('B. 私有字段访问检测');
await _auditPrivateFieldAccess();
await _section('C. 服务端逻辑完整性检测');
await _auditServerLogic();
await _section('D. 错误处理完备性检测');
await _auditErrorHandling();
await _section('E. 资源泄漏风险检测');
await _auditResourceLeaks();
await _section('F. 协议一致性检测');
await _auditProtocolConsistency();
await _section('G. 安全漏洞扫描');
await _auditSecurityVulnerabilities();
await _section('H. 降级链路完整性检测');
await _auditFallbackChains();
_printDeepSummary();
}
Future<void> _section(String title) async {
print('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print(' $title');
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
}
void _critical(String id, String name, String detail) {
_criticalCount++;
_deepResults.add(_DeepAuditResult(id, name, _Severity.critical, detail));
print(' 🔴 [CRITICAL] [$id] $name\n $detail');
}
void _high(String id, String name, String detail) {
_highCount++;
_deepResults.add(_DeepAuditResult(id, name, _Severity.high, detail));
print(' 🟠 [HIGH] [$id] $name\n $detail');
}
void _medium(String id, String name, String detail) {
_mediumCount++;
_deepResults.add(_DeepAuditResult(id, name, _Severity.medium, detail));
print(' 🟡 [MEDIUM] [$id] $name\n $detail');
}
bool _fileExists(String path) => File(path).existsSync();
String? _readFile(String path) =>
File(path).existsSync() ? File(path).readAsStringSync() : null;
List<String> _lines(String path) =>
File(path).existsSync() ? File(path).readAsLinesSync() : [];
int _lineCount(String path) =>
File(path).existsSync() ? _lines(path).length : 0;
// ══════════════════════════════════════════════════════════
// A. 空实现与桩代码检测
// ══════════════════════════════════════════════════════════
Future<void> _auditStubImplementations() async {
final blePath =
'lib/features/file_transfer/services/discovery/bluetooth_pairing_service.dart';
if (_fileExists(blePath)) {
final c = _readFile(blePath)!;
// 检测startAdvertising是否只是打日志
final startAdvIdx = c.indexOf('Future<void> startAdvertising');
if (startAdvIdx > 0) {
final methodBody = c.substring(startAdvIdx, startAdvIdx + 800);
final hasOnlyLogStatements =
methodBody.contains(
"Log.i('Bluetooth BLE: Advertising payload prepared",
) &&
methodBody.contains("Log.i('Bluetooth BLE: Advertising started") &&
!methodBody.contains('_advertisingManager') &&
!methodContentHasRealBleOperation(methodBody);
if (hasOnlyLogStatements) {
_critical(
'A1',
'BLE startAdvertising空实现',
'文件: bluetooth_pairing_service.dart\n'
'问题: startAdvertising方法体仅包含Log.i日志语句无实际BLE广播操作。\n'
'证据: "Note: flutter_blue_plus does not expose startAdvertising API"\n'
'影响: 用户无法通过BLE方式被其他设备发现配对成功率下降。\n'
'修复建议: 使用platform_channel调用原生API或集成nearby_connections插件。',
);
}
}
}
// TCP服务端 pause/resume 是否也是空实现
final tcpPath =
'lib/features/file_transfer/services/transport/tcp_socket_service.dart';
if (_fileExists(tcpPath)) {
final c = _readFile(tcpPath)!;
final hasPauseHandler = c.contains('_handlePause');
final hasResumeHandler = c.contains('_handleResume');
if (hasPauseHandler && hasResumeHandler) {
final pauseIdx = c.indexOf('_handlePause');
final resumeIdx = c.indexOf('_handleResume');
final pauseBody = pauseIdx > 0
? c.substring(pauseIdx, pauseIdx + 300)
: '';
final resumeBody = resumeIdx > 0
? c.substring(resumeIdx, resumeIdx + 300)
: '';
final pauseIsLogOnly =
pauseBody.contains('Log.') &&
!pauseBody.contains('_isPaused') &&
!pauseBody.contains('paused');
final resumeIsLogOnly =
resumeBody.contains('Log.') &&
!resumeBody.contains('_isPaused') &&
!resumeBody.contains('paused');
if (pauseIsLogOnly || resumeIsLogOnly) {
_high(
'A2',
'TCP服务端暂停/恢复空实现',
'文件: tcp_socket_service.dart\n'
'问题: ${pauseIsLogOnly ? "_handlePause" : ""}${resumeIsLogOnly ? "${pauseIsLogOnly ? "/" : ""}_handleResume" : ""} 方法体可能仅含日志。\n'
'影响: 接收端无法真正暂停传输,发送端暂停后接收端仍在写入文件导致数据损坏。',
);
}
}
}
// LocalSend HTTP pause/resume break检查
final provPath =
'lib/features/file_transfer/providers/transfer_provider.dart';
if (_fileExists(provPath)) {
final c = _readFile(provPath)!;
final lines = _lines(provPath);
bool foundBreakInPause = false;
bool foundBreakInResume = false;
for (int i = 0; i < lines.length; i++) {
if (lines[i].contains("case TransportType.localsendHttp:")) {
// Look ahead a few lines
for (int j = i + 1; j < (i + 10).clamp(0, lines.length); j++) {
if (lines[j].contains('break;') || lines[j].trim() == 'break;') {
if (i > 50 && lines[i - 1].contains('pause'))
foundBreakInPause = true;
if (i > 50 && lines[i - 1].contains('resume'))
foundBreakInResume = true;
break;
}
}
}
}
if (foundBreakInPause || foundBreakInResume) {
_medium(
'A3',
'LocalSend HTTP pause/resume为break',
'文件: transfer_provider.dart\n'
'观察: ${foundBreakInPause ? "pause" : ""}${foundBreakInResume ? "${foundBreakInPause ? "/" : ""}resume" : ""} 操作对localsendHttp使用break(no-op)。\n'
'评估: HTTP协议确实无法暂停已发出的请求这是合理的设计选择。\n'
'建议: 在UI层提示用户"HTTP传输不支持暂停"。',
);
}
}
}
bool methodContentHasRealBleOperation(String body) {
return body.contains('advertise') ||
body.contains('startAdvertising') ||
body.contains('peripheral') ||
body.contains('BLEAdvertisement') ||
body.contains('methodChannel') ||
body.contains('nearby');
}
// ══════════════════════════════════════════════════════════
// B. 私有字段访问检测
// ══════════════════════════════════════════════════════════
Future<void> _auditPrivateFieldAccess() async {
final webrtcPath =
'lib/features/file_transfer/services/transport/webrtc_service.dart';
if (_fileExists(webrtcPath)) {
final c = _readFile(webrtcPath)!;
// 检测 _bytesReceived 私有字段访问
if (c.contains('receiver._bytesReceived')) {
_high(
'B1',
'WebRTC访问_receiver私有字段',
'文件: webrtc_service.dart\n'
'问题: 使用 receiver._bytesReceived 访问外部类的私有字段。\n'
'风险: Dart的_前缀表示库级私有同库内可以访问但违反封装原则。\n'
'如果WebRtcReceiver重构或字段改名将导致编译失败。\n'
'修复建议: 为WebRtcReceiver添加公共getter如 bytesReceived。',
);
}
// 检测其他可能的私有访问
final privateAccessPatterns = [RegExp(r'\w+\._\w+')];
int privateAccessCount = 0;
for (final pattern in privateAccessPatterns) {
privateAccessCount += pattern
.allMatches(c)
.where(
(m) =>
m.group(0)!.contains('_bytesReceived') ||
m.group(0)!.contains('_buffer') ||
m.group(0)!.contains('_file'),
)
.length;
}
if (privateAccessCount > 2) {
_medium(
'B2',
'多处私有字段访问',
'文件: webrtc_service.dart\n'
'观察: 发现$privateAccessCount处可能的私有字段访问\n'
'建议: 统一通过公开接口访问内部状态。',
);
}
}
}
// ══════════════════════════════════════════════════════════
// C. 服务端逻辑完整性检测
// ══════════════════════════════════════════════════════════
Future<void> _auditServerLogic() async {
// PHP API完整性
final phpPath = 'docs/toolsapi/application/api/controller/FileTransfer.php';
if (_fileExists(phpPath)) {
final c = _readFile(phpPath)!;
final requiredMethods = [
'turn_credentials',
'signaling_info',
'pair_request',
'pair_accept',
'pair_reject',
'paired_devices',
'pair_delete',
'localsend_info',
'health_check',
'room_create',
'room_join',
'room_status',
];
int foundMethods = 0;
final missingMethods = <String>[];
for (final m in requiredMethods) {
if (c.contains('function $m') || c.contains("function $m")) {
foundMethods++;
} else {
missingMethods.add(m);
}
}
if (foundMethods >= 9) {
_medium(
'C1',
'PHP API方法覆盖($foundMethods/${requiredMethods.length})',
'文件: FileTransfer.php\n'
'已实现: $foundMethods个API方法\n'
'${missingMethods.isEmpty ? "全部覆盖 ✅" : "缺少: ${missingMethods.join(", ")}"}\n'
'额外: 速率限制(60次/分钟) + CORS + TURN凭据发放(HMAC-SHA1)',
);
} else {
_high('C2', 'PHP API方法不完整', '缺少方法: ${missingMethods.join(", ")}');
}
// 安全检查: 硬编码凭据
if (c.contains("xianyan-turn-secret-2026") || c.contains("520Kiss123")) {
_high(
'C3',
'服务端硬编码敏感信息',
'文件: FileTransfer.php / deploy_coturn.py\n'
'问题: TURN密钥和SSH密码硬编码在源码中。\n'
'风险: 如果代码泄露则TURN服务器可被滥用。\n'
'修复建议: 使用环境变量或加密配置文件存储敏感信息。',
);
}
}
// WebSocket信令服务器完整性
final wsPath = 'docs/toolsapi/signaling/server.js';
if (_fileExists(wsPath)) {
final c = _readFile(wsPath)!;
final wsLineCount = _lineCount(wsPath);
final hasRelay = c.contains('handleRelay') || c.contains('relay-data');
final hasRoomManagement =
c.contains('handleCreateRoom') && c.contains('handleJoinRoom');
final hasHeartbeat = c.contains('heartbeat_ack');
final hasCleanup = c.contains('cleanupExpiredRooms');
final hasSignalingForward = c.contains('handleSignaling');
final hasRateLimit = c.contains('checkRateLimit');
final hasTlsSupport =
c.contains('tlsCert') || c.contains('https.createServer');
if (wsLineCount > 200 &&
hasRelay &&
hasRoomManagement &&
hasHeartbeat &&
hasCleanup) {
_medium(
'C4',
'WebSocket信令服务器(${wsLineCount}行)',
'文件: signaling/server.js\n'
'✅ 房间管理(create/join/status)\n'
'✅ 信令转发(offer/answer/iceCandidate)\n'
'✅ Relay中继(relay-data/relay-binary)\n'
'✅ 心跳保活(heartbeat_ack)\n'
'✅ 过期房间清理(cleanupExpiredRooms)\n'
'✅ 速率限制(checkRateLimit)\n'
'${hasTlsSupport ? "✅ TLS支持" : "⚠️ 未启用TLS"}',
);
} else {
_high(
'C5',
'WebSocket信令服务器不完整',
'行数: $wsLineCount | Relay:$hasRelay | Room:$hasRoomManagement | Heartbeat:$hasHeartbeat',
);
}
}
}
// ══════════════════════════════════════════════════════════
// D. 错误处理完备性检测
// ══════════════════════════════════════════════════════════
Future<void> _auditErrorHandling() async {
// 检查关键service的错误处理覆盖率
final services = [
(
'lib/features/file_transfer/services/transport/webrtc_service.dart',
'WebRTC',
),
(
'lib/features/file_transfer/services/transport/tcp_socket_service.dart',
'TCP',
),
(
'lib/features/file_transfer/services/transport/localsend_service.dart',
'LocalSend',
),
(
'lib/features/file_transfer/services/security/tls_security_service.dart',
'TLS',
),
];
for (final entry in services) {
final path = entry.$1;
final name = entry.$2;
if (!_fileExists(path)) continue;
final c = _readFile(path)!;
final lineCount = _lineCount(path);
final tryCount = RegExp(r'\btry\s*\{').allMatches(c).length;
final catchCount = RegExp(r'\bcatch\s*\(').allMatches(c).length;
final finallyCount = RegExp(r'\bfinally\s*\{').allMatches(c).length;
final logErrorCount = RegExp(r'Log\.e\s*\(').allMatches(c).length;
final errorRatio = lineCount > 0
? (tryCount / lineCount * 100).toStringAsFixed(1)
: '0';
if (tryCount < 3 && lineCount > 100) {
_high(
'D1-$name',
'$name Service错误处理不足',
'文件: ${path.split("/").last}\n'
'行数: $lineCount | try块: $tryCount | catch块: $catchCount | Log.e: $logErrorCount\n'
'错误处理密度: $errorRatio%\n'
'建议: 关键IO操作(网络/文件)应包裹try-catch。',
);
} else {
_medium(
'D1-$name',
'$name Service错误处理',
'行数: $lineCount | try/catch: $tryCount/$catchCount | Log.e: $logErrorCount | 密度: $errorRatio%',
);
}
}
// 检查空指针防护
final providerPath =
'lib/features/file_transfer/providers/transfer_provider.dart';
if (_fileExists(providerPath)) {
final c = _readFile(providerPath)!;
final nullCheckBeforeAccess = RegExp(r'\?.\w+').allMatches(c).length;
final nullCoalescing = RegExp(r'\?\?').allMatches(c).length;
final bangOperator = RegExp(r'!').allMatches(
RegExp(r'(?:\w|\])!').allMatches(c).map((m) => m.group(0)!).join(),
);
if (nullCheckBeforeAccess + nullCoalescing < 20 &&
_lineCount(providerPath) > 200) {
_medium(
'D2',
'Provider空指针防护',
'文件: transfer_provider.dart\n'
'?.安全访问: $nullCheckBeforeAccess次 | ??空合并: $nullCoalescing次\n'
'建议: 对来自外部的数据(device/task/message)增加空检查。',
);
}
}
}
// ══════════════════════════════════════════════════════════
// E. 资源泄漏风险检测
// ══════════════════════════════════════════════════════════
Future<void> _auditResourceLeaks() async {
// 检查StreamSubscription是否有cancel
final pairPath = 'lib/features/file_transfer/services/pairing_service.dart';
if (_fileExists(pairPath)) {
final c = _readFile(pairPath)!;
final listenCount = RegExp(r'\.listen\s*\(').allMatches(c).length;
final cancelCount = RegExp(r'\.cancel\s*\(').allMatches(c).length;
final disposeCount = RegExp(r'dispose\s*\(').allMatches(c).length;
if (listenCount > cancelCount + 2) {
_high(
'E1',
'PairingService潜在订阅泄漏',
'文件: pairing_service.dart\n'
'.listen(): $listenCount次 | .cancel(): $cancelCount次 | .dispose(): $disposeCount次\n'
'差异: ${listenCount - cancelCount}个未匹配的listen\n'
'注意: 如果_stopDiscovery中有统一清理则可能是安全的。',
);
}
}
// 临时文件清理检查
final provPath =
'lib/features/file_transfer/providers/transfer_provider.dart';
if (_fileExists(provPath)) {
final c = _readFile(provPath)!;
final hasTempFileCreate =
c.contains('createTemp') ||
c.contains('temporaryDirectory') ||
c.contains('.txt');
final hasTempFileDelete =
c.contains('.delete()') || c.contains('deleteIfExists');
if (hasTempFileCreate && !hasTempFileDelete) {
_critical(
'E2',
'文本消息临时文件未删除',
'文件: transfer_provider.dart\n'
'问题: 文本消息降级到LocalSend时会创建临时.txt文件但未找到.delete()调用。\n'
'风险: 频繁发送消息会在临时目录积累大量小文件。\n'
'影响: 长时间运行后磁盘空间被占满。\n'
'修复建议: 在finally块中添加tempFile.delete();',
);
} else if (hasTempFileCreate && hasTempFileDelete) {
_medium('E2+', '文本消息临时文件有清理', '观察到临时文件创建和删除操作同时存在。');
}
}
// WebSocket连接关闭检查
final sigPath = 'lib/features/file_transfer/services/signaling_service.dart';
if (_fileExists(sigPath)) {
final c = _readFile(sigPath)!;
final hasDisconnectHandler =
c.contains('onDone') || c.contains('disconnect');
final hasReconnectOnClose = c.contains('_scheduleReconnect');
final hasDisposeMethod = c.contains('dispose()') || c.contains('close()');
if (!hasDisconnectHandler || !hasDisposeMethod) {
_high(
'E3',
'WebSocket连接生命周期管理',
'文件: signaling_service.dart\n'
'onDone处理: $hasDisconnectHandler | 重连机制: $hasReconnectOnClose | dispose: $hasDisposeMethod\n'
'建议: 确保应用退出时正确关闭WebSocket连接释放资源。',
);
}
}
}
// ══════════════════════════════════════════════════════════
// F. 协议一致性检测
// ══════════════════════════════════════════════════════════
Future<void> _auditProtocolConsistency() async {
// LocalSend v1 vs v2 路由一致性
final lsPath =
'lib/features/file_transfer/services/transport/localsend_service.dart';
if (_fileExists(lsPath)) {
final c = _readFile(lsPath)!;
final v1Routes = ['register', 'send-request', 'send', 'cancel', 'info'];
final v2Routes = ['prepare-upload', 'upload-file', 'finish-upload'];
int v1Implemented = 0;
for (final route in v1Routes) {
if (c.contains("/$route") || c.contains("'$route'")) v1Implemented++;
}
int v2Implemented = 0;
for (final route in v2Routes) {
if (c.contains("/$route") || c.contains("'$route'")) v2Implemented++;
}
_medium(
'F1',
'LocalSend协议路由覆盖',
'v1 API: $v1Implemented/${v1Routes.length} (${v1Routes.join(", ")})\n'
'v2 API: $v2Implemented/${v2Routes.length} (${v2Routes.join(", ")})\n'
'${v1Implemented == v1Routes.length && v2Implemented == v2Routes.length ? "✅ 双版本完整" : "⚠️ 存在缺失路由"}',
);
}
// TCP帧类型定义与处理一致性
final tcpPath =
'lib/features/file_transfer/services/transport/tcp_socket_service.dart';
if (_fileExists(tcpPath)) {
final c = _readFile(tcpPath)!;
final frameTypes = [
'handshake',
'meta',
'data',
'ack',
'pause',
'resume',
'cancel',
'error',
'message',
];
final handlers = [
'handleHandshake',
'handleMeta',
'handleData',
'handleAck',
'handlePause',
'handleResume',
'handleCancel',
'handleErrorMessage',
'handleMessage',
];
int matchedHandlers = 0;
for (int i = 0; i < frameTypes.length && i < handlers.length; i++) {
if (c.contains(handlers[i])) matchedHandlers++;
}
if (matchedHandlers < frameTypes.length - 2) {
_high(
'F2',
'TCP帧类型处理器不完整',
'帧类型: ${frameTypes.length}\n'
'处理器: $matchedHandlers/${frameTypes.length}\n'
'缺失: ${frameTypes.sublist(matchedHandlers).join(", ")}',
);
} else {
_medium(
'F2+',
'TCP帧处理器覆盖',
'$matchedHandlers/${frameTypes.length} 帧类型有对应处理器',
);
}
}
}
// ══════════════════════════════════════════════════════════
// G. 安全漏洞扫描
// ══════════════════════════════════════════════════════════
Future<void> _auditSecurityVulnerabilities() async {
// TLS证书验证
final tlsPath =
'lib/features/file_transfer/services/security/tls_security_service.dart';
if (_fileExists(tlsPath)) {
final c = _readFile(tlsPath)!;
final hasTrustAll =
c.contains('trustAll') || c.contains('badCertificateCallback');
final hasCertPinning =
c.contains('certificatePin') || c.contains('fingerprint');
if (hasTrustAll && !hasCertPinning) {
_critical(
'G1',
'TLS证书信任策略过于宽松',
'文件: tls_security_service.dart\n'
'问题: 检测到 trustAll 或 badCertificateCallback 始终返回true。\n'
'风险: 中间人攻击(MitM)可截获所有传输数据。\n'
'场景: 公共WiFi环境下用户传输敏感文件时可被窃听。\n'
'修复建议: 实现证书指纹固定(Certificate Pinning),至少在首次连接后保存服务器证书指纹。',
);
}
}
// 路径遍历防护
final localsendPath =
'lib/features/file_transfer/services/transport/localsend_service.dart';
if (_fileExists(localsendPath)) {
final c = _readFile(localsendPath)!;
final hasPathValidation =
c.contains('pathTraversal') ||
c.contains('..') &&
(c.contains('sanitize') ||
c.contains('normalize') ||
c.contains('contains("..")'));
final hasFileNameSanitize =
c.contains('basename') || c.contains('path.basename');
if (!hasPathValidation) {
_high(
'G2',
'文件路径未做遍历防护',
'文件: localsend_service.dart\n'
'问题: 接收到的文件名/路径未进行路径遍历检查(../)。\n'
'风险: 攻击者可通过恶意文件名写入系统任意位置(如../../etc/crontab)。\n'
'修复建议: 使用path.basename提取纯文件名拒绝包含..的路径。',
);
}
}
// 凭据硬编码检查
final allFiles = [
'docs/toolsapi/scripts/deploy_coturn.py',
'docs/toolsapi/application/api/controller/FileTransfer.php',
];
for (final f in allFiles) {
if (_fileExists(f)) {
final c = _readFile(f)!;
if (RegExp(
r'(password|passwd|secret|token|key)\s*[:=]\s*["\x27][^"\x27]{6,}',
).hasMatch(c)) {
_high(
'G3',
'敏感信息硬编码: ${f.split("/").last}',
'文件: $f\n'
'问题: 检测到密码/密钥/Token等敏感信息以明文硬编码。\n'
'建议: 迁移至环境变量或加密配置存储。',
);
}
}
}
}
// ══════════════════════════════════════════════════════════
// H. 降级链路完整性检测
// ══════════════════════════════════════════════════════════
Future<void> _auditFallbackChains() async {
// 文本消息降级链
final provPath =
'lib/features/file_transfer/providers/transfer_provider.dart';
if (_fileExists(provPath)) {
final c = _readFile(provPath)!;
// 检查三级降级链
final chain1 = c.contains('isMessageChannelOpen'); // WebRTC DataChannel
final chain2 =
c.contains('signalingService.isConnected') ||
c.contains('signalingService.isReady'); // Signaling WebSocket
final chain3 =
c.contains('localSendService.sendFile') ||
c.contains('localsendService.sendFile'); // LocalSend HTTP fallback
if (chain1 && chain2 && chain3) {
// 检查每级之间是否有错误处理和日志
final hasFallbackLog =
c.contains('降级') ||
c.contains('fallback') ||
c.contains('尝试') ||
c.contains('备用');
final hasUserNotification =
c.contains('ScaffoldMessenger') ||
c.contains('toast') ||
c.contains('showSnackBar') ||
c.contains('文本发送失败');
if (hasUserNotification) {
_medium(
'H1',
'文本消息降级链完整',
'✅ 第一级: WebRTC DataChannel (isMessageChannelOpen)\n'
'✅ 第二级: Signaling WebSocket (signalingService.isConnected)\n'
'✅ 第三级: LocalSend HTTP (创建临时文件发送)\n'
'✅ 用户通知: 有失败提示\n'
'${hasFallbackLog ? "✅ 降级日志: 有记录" : "⚠️ 缺少降级日志"}',
);
} else {
_high('H2', '文本消息降级链缺用户通知', '三级降级链存在但失败时可能无用户可见提示。');
}
} else {
_critical(
'H3',
'文本消息降级链断裂',
'第一级(WebRTC): $chain1\n'
'第二级(Signaling): $chain2\n'
'第三级(LocalSend): $chain3\n'
'缺失的级别需要补全。',
);
}
}
// 传输路由降级 (selectRoute alternatives)
final routerPath =
'lib/features/file_transport/services/transport/transport_router.dart';
final routerPathAlt =
'lib/features/file_transfer/services/transport/transport_router.dart';
final actualRouterPath = _fileExists(routerPath)
? routerPath
: (_fileExists(routerPathAlt) ? routerPathAlt : null);
if (actualRouterPath != null) {
final c = _readFile(actualRouterPath)!;
final hasAlternatives = c.contains('alternatives');
final hasConfidenceScore = c.contains('confidence');
if (!hasAlternatives && hasConfidenceScore) {
_high(
'H4',
'传输路由无备选方案',
'文件: transport_router.dart\n'
'问题: selectRoute返回confidence但未提供alternatives备选列表。\n'
'影响: 当首选路由失败时无法自动切换到备选方案。\n'
'这与任务5.8"传输路由自动降级"的要求不符。',
);
}
}
}
void _printDeepSummary() {
print('\n\n╔══════════════════════════════════════════════════════════════╗');
print('║ 深度逻辑审计结果汇总 ║');
print('╠══════════════════════════════════════════════════════════════╣');
print('║ 🔴 CRITICAL: $_criticalCount (功能性缺陷) ');
print('║ 🟠 HIGH: $_highCount (严重质量问题) ');
print('║ 🟡 MEDIUM: $_mediumCount (代码规范) ');
print(
'║ 📊 总计: ${_criticalCount + _highCount + _mediumCount} ',
);
print('╚══════════════════════════════════════════════════════════════╝');
if (_criticalCount > 0) {
print('\n 🔴 CRITICAL项(必须立即修复):');
for (final r in _deepResults.where(
(r) => r.severity == _Severity.critical,
)) {
print(' ┌─ [${r.id}] ${r.name}');
print('${r.detail.replaceAll('\n', '\n')}');
print(' └─');
}
}
if (_highCount > 0) {
print('\n 🟠 HIGH项(应尽快修复):');
for (final r in _deepResults.where((r) => r.severity == _Severity.high)) {
print(' ┌─ [${r.id}] ${r.name}');
print('${r.detail.replaceAll('\n', '\n')}');
print(' └─');
}
}
if (_mediumCount > 0) {
print('\n 🟡 MEDIUM项(建议优化):');
for (final r in _deepResults.where((r) => r.severity == _Severity.medium)) {
print(' • [${r.id}] ${r.name}: ${r.detail.split('\n').first}');
}
}
print('\n ═════════════════════════════════════════════════════');
print(' 审计结论:');
if (_criticalCount >= 3) {
print(' ⛔ 存在多个严重功能缺陷,不建议发布当前版本!');
} else if (_criticalCount > 0) {
print(' ⚠️ 发现$_criticalCount个关键缺陷,需优先修复后再发布。');
} else if (_highCount > 3) {
print(' ✅ 核心功能基本完整,但有$_highCount处需改进的质量问题');
} else {
print(' ✅ 代码质量良好,仅有少量优化建议。');
}
}
enum _Severity { critical, high, medium }
class _DeepAuditResult {
final String id;
final String name;
final _Severity severity;
final String detail;
_DeepAuditResult(this.id, this.name, this.severity, this.detail);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,319 @@
// ============================================================
// 闲言APP — LocalSend传输接口验证脚本
// 创建时间: 2026-05-11
// 更新时间: 2026-05-11
// 作用: 验证LocalSend HTTP服务器端点+HTTPS→HTTP降级+信令服务连接
// 上次更新: 初始创建
// ============================================================
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
void main(List<String> args) async {
final targetIp = args.isNotEmpty ? args[0] : '10.0.0.7';
final targetPort = args.length > 1 ? int.parse(args[1]) : 53317;
print('╔══════════════════════════════════════════════════╗');
print('║ LocalSend 传输接口验证脚本 v1.0 ║');
print('╚══════════════════════════════════════════════════╝');
print('');
print('目标设备: $targetIp:$targetPort');
print('');
final dio = Dio(
BaseOptions(
connectTimeout: const Duration(seconds: 5),
sendTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
validateStatus: (status) => status != null && status < 600,
),
);
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
final client = HttpClient();
client.badCertificateCallback = (cert, host, port) => true;
return client;
};
final results = <String, bool>{};
// ─── 1. 测试 HTTPS /api/localsend/v2/info ────────────────
print('━━━ 1. HTTPS v2/info ─━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
try {
final resp = await dio.get(
'https://$targetIp:$targetPort/api/localsend/v2/info',
);
print(' ✅ HTTPS v2/info: ${resp.statusCode}');
print(' 📦 ${jsonEncode(resp.data)}');
results['https_v2_info'] = resp.statusCode == 200;
} on DioException catch (e) {
print(' ❌ HTTPS v2/info 失败: ${e.type} ${e.message}');
results['https_v2_info'] = false;
}
// ─── 2. 测试 HTTP /api/localsend/v2/info ────────────────
print('');
print('━━━ 2. HTTP v2/info ─━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
try {
final resp = await dio.get(
'http://$targetIp:$targetPort/api/localsend/v2/info',
);
print(' ✅ HTTP v2/info: ${resp.statusCode}');
print(' 📦 ${jsonEncode(resp.data)}');
results['http_v2_info'] = resp.statusCode == 200;
} on DioException catch (e) {
print(' ❌ HTTP v2/info 失败: ${e.type} ${e.message}');
results['http_v2_info'] = false;
}
// ─── 3. 测试 HTTPS /api/localsend/v1/info ────────────────
print('');
print('━━━ 3. HTTPS v1/info ─━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
try {
final resp = await dio.get(
'https://$targetIp:$targetPort/api/localsend/v1/info',
);
print(' ✅ HTTPS v1/info: ${resp.statusCode}');
print(' 📦 ${jsonEncode(resp.data)}');
results['https_v1_info'] = resp.statusCode == 200;
} on DioException catch (e) {
print(' ❌ HTTPS v1/info 失败: ${e.type} ${e.message}');
results['https_v1_info'] = false;
}
// ─── 4. 测试 HTTP /api/localsend/v1/info ────────────────
print('');
print('━━━ 4. HTTP v1/info ─━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
try {
final resp = await dio.get(
'http://$targetIp:$targetPort/api/localsend/v1/info',
);
print(' ✅ HTTP v1/info: ${resp.statusCode}');
print(' 📦 ${jsonEncode(resp.data)}');
results['http_v1_info'] = resp.statusCode == 200;
} on DioException catch (e) {
print(' ❌ HTTP v1/info 失败: ${e.type} ${e.message}');
results['http_v1_info'] = false;
}
// ─── 5. 测试 HTTPS 文本消息 ──────────────────────────────
print('');
print('━━━ 5. HTTPS v2/message ─━━━━━━━━━━━━━━━━━━━━━━━━━━');
try {
final resp = await dio.post(
'https://$targetIp:$targetPort/api/localsend/v2/message',
data: {
'senderAlias': '验证脚本',
'senderFingerprint': 'test-script',
'text': '🔍 接口验证测试消息',
'sessionId': 'verify-${DateTime.now().millisecondsSinceEpoch}',
},
options: Options(contentType: Headers.jsonContentType),
);
print(' ✅ HTTPS v2/message: ${resp.statusCode}');
print(' 📦 ${jsonEncode(resp.data)}');
results['https_v2_message'] = resp.statusCode == 200;
} on DioException catch (e) {
print(' ❌ HTTPS v2/message 失败: ${e.type} ${e.message}');
results['https_v2_message'] = false;
}
// ─── 6. 测试 HTTP 文本消息 ──────────────────────────────
print('');
print('━━━ 6. HTTP v2/message ─━━━━━━━━━━━━━━━━━━━━━━━━━━━');
try {
final resp = await dio.post(
'http://$targetIp:$targetPort/api/localsend/v2/message',
data: {
'senderAlias': '验证脚本',
'senderFingerprint': 'test-script',
'text': '🔍 HTTP接口验证测试消息',
'sessionId': 'verify-http-${DateTime.now().millisecondsSinceEpoch}',
},
options: Options(contentType: Headers.jsonContentType),
);
print(' ✅ HTTP v2/message: ${resp.statusCode}');
print(' 📦 ${jsonEncode(resp.data)}');
results['http_v2_message'] = resp.statusCode == 200;
} on DioException catch (e) {
print(' ❌ HTTP v2/message 失败: ${e.type} ${e.message}');
results['http_v2_message'] = false;
}
// ─── 7. 测试 HTTPS prepare-upload ────────────────────────
print('');
print('━━━ 7. HTTPS v2/prepare-upload ─━━━━━━━━━━━━━━━━━━━');
try {
final resp = await dio.post(
'https://$targetIp:$targetPort/api/localsend/v2/prepare-upload',
data: {
'senderInfo': {
'alias': '验证脚本',
'fingerprint': 'test-script',
'deviceModel': 'Script',
'deviceType': 'desktop',
},
'files': {
'file-0': {
'id': 'file-0',
'fileName': 'test.txt',
'size': 13,
'sha256':
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
},
},
},
options: Options(contentType: Headers.jsonContentType),
);
print(' ✅ HTTPS v2/prepare-upload: ${resp.statusCode}');
print(' 📦 ${jsonEncode(resp.data)}');
results['https_v2_prepare'] = resp.statusCode == 200;
} on DioException catch (e) {
print(' ❌ HTTPS v2/prepare-upload 失败: ${e.type} ${e.message}');
results['https_v2_prepare'] = false;
}
// ─── 8. 测试 HTTP prepare-upload ────────────────────────
print('');
print('━━━ 8. HTTP v2/prepare-upload ─━━━━━━━━━━━━━━━━━━━━');
try {
final resp = await dio.post(
'http://$targetIp:$targetPort/api/localsend/v2/prepare-upload',
data: {
'senderInfo': {
'alias': '验证脚本',
'fingerprint': 'test-script',
'deviceModel': 'Script',
'deviceType': 'desktop',
},
'files': {
'file-0': {
'id': 'file-0',
'fileName': 'test.txt',
'size': 13,
'sha256':
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
},
},
},
options: Options(contentType: Headers.jsonContentType),
);
print(' ✅ HTTP v2/prepare-upload: ${resp.statusCode}');
print(' 📦 ${jsonEncode(resp.data)}');
results['http_v2_prepare'] = resp.statusCode == 200;
} on DioException catch (e) {
print(' ❌ HTTP v2/prepare-upload 失败: ${e.type} ${e.message}');
results['http_v2_prepare'] = false;
}
// ─── 9. 测试信令服务器连接 ──────────────────────────────
print('');
print('━━━ 9. 信令服务器连接 ─━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
const signalingUrls = [
'wss://tools.wktyl.com:9443',
'ws://tools.wktyl.com:9443',
'wss://tools.wktyl.com/ws',
];
for (final url in signalingUrls) {
final uri = Uri.parse(url);
try {
final socket = await WebSocket.connect(
url,
).timeout(const Duration(seconds: 5));
print('$url 连接成功');
socket.close();
results['signaling_${uri.scheme}'] = true;
} catch (e) {
print('$url 连接失败: $e');
results['signaling_${uri.scheme}'] = false;
}
}
// ─── 10. 测试服务器API信令信息 ──────────────────────────
print('');
print('━━━ 10. 服务器API信令信息 ─━━━━━━━━━━━━━━━━━━━━━━━━');
try {
final resp = await dio.get(
'https://tools.wktyl.com/api/file_transfer/signaling_info',
);
print(' ✅ signaling_info: ${resp.statusCode}');
print(' 📦 ${jsonEncode(resp.data)}');
results['api_signaling_info'] = true;
} on DioException catch (e) {
print(' ❌ signaling_info 失败: ${e.type} ${e.message}');
results['api_signaling_info'] = false;
}
// ─── 11. 测试本机HTTP服务器 ──────────────────────────────
print('');
print('━━━ 11. 本机HTTP服务器 ─━━━━━━━━━━━━━━━━━━━━━━━━━━━');
final localIps = await _getLocalIps();
print(' 📋 本机IP: ${localIps.join(", ")}');
for (final ip in localIps) {
try {
final resp = await dio.get(
'http://$ip:$targetPort/api/localsend/v2/info',
);
print(' ✅ 本机 $ip:$targetPort: ${resp.statusCode}');
results['local_http_$ip'] = true;
} catch (e) {
print(' ❌ 本机 $ip:$targetPort: $e');
results['local_http_$ip'] = false;
}
}
// ─── 汇总 ────────────────────────────────────────────────
print('');
print('╔══════════════════════════════════════════════════╗');
print('║ 验证结果汇总 ║');
print('╚══════════════════════════════════════════════════╝');
int pass = 0, fail = 0;
results.forEach((name, ok) {
final icon = ok ? '' : '';
print(' $icon $name');
if (ok)
pass++;
else
fail++;
});
print('');
print(' 通过: $pass / ${results.length}');
if (fail > 0) {
print('');
print(' ⚠️ 失败项分析:');
if (results['https_v2_info'] == false && results['http_v2_info'] == true) {
print(' → HTTPS不可用但HTTP可用: 服务器未启用HTTPS客户端必须降级HTTP');
}
if (results['https_v2_info'] == false && results['http_v2_info'] == false) {
print(' → HTTPS和HTTP都不可用: 目标设备未运行传输服务');
}
if (results['signaling_wss'] == false) {
print(' → 信令服务器不可达: 端口9443可能被防火墙阻止或服务未运行');
}
if (results['http_v2_message'] == false &&
results['http_v2_info'] == true) {
print(' → info可用但message不可用: message端点可能未注册或实现有误');
}
}
exit(fail > 0 ? 1 : 0);
}
Future<List<String>> _getLocalIps() async {
final ips = <String>[];
try {
final result = await Process.run('ipconfig', []);
final output = result.stdout.toString();
final ipv4Regex = RegExp(r'IPv4 Address[.\s]*:\s*(\d+\.\d+\.\d+\.\d+)');
for (final match in ipv4Regex.allMatches(output)) {
ips.add(match.group(1)!);
}
} catch (_) {}
if (ips.isEmpty) ips.add('127.0.0.1');
return ips;
}