chore: 批量代码优化与功能迭代更新
本次提交包含大量代码优化、功能新增与服务端配置更新: 1. 修复分析报告统计数据,调整CMake策略设置 2. 优化APP权限配置、编辑器与聊天界面组件 3. 更新依赖库版本与pubspec配置 4. 新增文件传输服务端、信令服务器相关配置与脚本 5. 完善用户注销功能与数据库迁移脚本 6. 优化多处动画效果、代码风格与日志输出 7. 新增多种调试与部署脚本,修复已知BUG
This commit is contained in:
60
scripts/audit_report_1778436032014.md
Normal file
60
scripts/audit_report_1778436032014.md
Normal 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% |
|
||||
|
||||
---
|
||||
*报告由自动化审计脚本生成, 基于静态代码分析*
|
||||
1224
scripts/file_transfer_audit.dart
Normal file
1224
scripts/file_transfer_audit.dart
Normal file
File diff suppressed because it is too large
Load Diff
810
scripts/file_transfer_deep_audit.dart
Normal file
810
scripts/file_transfer_deep_audit.dart
Normal 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);
|
||||
}
|
||||
1287
scripts/file_transfer_verify.dart
Normal file
1287
scripts/file_transfer_verify.dart
Normal file
File diff suppressed because it is too large
Load Diff
319
scripts/localsend_interface_verify.dart
Normal file
319
scripts/localsend_interface_verify.dart
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user