надеюсь будет работать сукi

This commit is contained in:
Jganenokk
2026-07-05 23:43:15 +07:00
parent 7ce7cf7a1d
commit 815e41648a
10 changed files with 419 additions and 117 deletions
+37 -8
View File
@@ -1,6 +1,9 @@
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import '../storage/token_storage.dart';
import '../utils/logger.dart';
enum ProxyType { none, socks5, httpConnect }
@@ -35,26 +38,52 @@ abstract class ProxyConfig {
static const String _prefUsername = 'proxy_username';
static const String _prefPassword = 'proxy_password';
static const Duration _secureReadTimeout = Duration(seconds: 5);
static Future<String?> _readSecureSafe(String key) async {
try {
return await TokenStorage.readSecure(key).timeout(_secureReadTimeout);
} catch (e) {
logger.w('ProxyConfig: чтение secure "$key" не удалось/зависло: $e');
return null;
}
}
static Future<void> _migrateLegacySecure(String key, String value) async {
try {
await TokenStorage.writeSecure(key, value).timeout(_secureReadTimeout);
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
} catch (e) {
logger.w('ProxyConfig: миграция legacy "$key" не удалась: $e');
}
}
static Future<ProxySettings> load() async {
final prefs = await SharedPreferences.getInstance();
final typeIndex = prefs.getInt(_prefType) ?? 0;
final host = prefs.getString(_prefHost) ?? '';
final port = prefs.getInt(_prefPort) ?? 1080;
var username = await TokenStorage.readSecure(_prefUsername);
var password = await TokenStorage.readSecure(_prefPassword);
if (ProxyType.values[typeIndex.clamp(0, ProxyType.values.length - 1)] ==
ProxyType.none ||
host.isEmpty) {
return ProxySettings(
type: ProxyType.values[typeIndex.clamp(0, ProxyType.values.length - 1)],
host: host,
port: port,
);
}
var username = await _readSecureSafe(_prefUsername);
var password = await _readSecureSafe(_prefPassword);
final legacyUsername = prefs.getString(_prefUsername);
final legacyPassword = prefs.getString(_prefPassword);
if (username == null && legacyUsername != null) {
username = legacyUsername;
await TokenStorage.writeSecure(_prefUsername, legacyUsername);
unawaited(_migrateLegacySecure(_prefUsername, legacyUsername));
}
if (password == null && legacyPassword != null) {
password = legacyPassword;
await TokenStorage.writeSecure(_prefPassword, legacyPassword);
}
if (legacyUsername != null || legacyPassword != null) {
await prefs.remove(_prefUsername);
await prefs.remove(_prefPassword);
unawaited(_migrateLegacySecure(_prefPassword, legacyPassword));
}
return ProxySettings(
type: ProxyType.values[typeIndex.clamp(0, ProxyType.values.length - 1)],
+38 -10
View File
@@ -15,6 +15,8 @@ enum SocketState { disconnected, connecting, connected }
/// Отдаёт сырые байты через [dataStream], сборкой пакетов занимается [PacketReceiver].
class Connection {
static const Duration _defaultConnectTimeout = Duration(seconds: 15);
static const Duration _proxyLoadTimeout = Duration(seconds: 8);
static const Duration _vpnCallTimeout = Duration(seconds: 5);
SecureSocket? _socket;
StreamSubscription<Uint8List>? _subscription;
@@ -40,20 +42,41 @@ class Connection {
bool bypassVpn = false,
Duration? timeout,
}) async {
if (_state != SocketState.disconnected) return;
if (_state != SocketState.disconnected) {
logger.w('Connection.connect пропущен: state=$_state (уже $_state)');
return;
}
_setState(SocketState.connecting);
try {
final proxySettings = await ProxyConfig.load();
// Решение «обходить VPN или нет» принимает вызывающий (Api):
// первая попытка идёт через VPN, при её провале — мимо туннеля.
if (bypassVpn) {
await VpnBypassService.instance.bind();
} else {
await VpnBypassService.instance.restoreDefault();
logger.i('Connection: загрузка прокси-конфига');
ProxySettings proxySettings;
try {
proxySettings = await ProxyConfig.load().timeout(_proxyLoadTimeout);
} catch (e) {
logger.w('Connection: ProxyConfig.load завис/упал ($e) — без прокси');
proxySettings = const ProxySettings();
}
logger.i(
'Connection: VPN ${bypassVpn ? 'bind (обход)' : 'restoreDefault'}',
);
try {
if (bypassVpn) {
await VpnBypassService.instance.bind().timeout(_vpnCallTimeout);
} else {
await VpnBypassService.instance
.restoreDefault()
.timeout(_vpnCallTimeout);
}
} catch (e) {
logger.w('Connection: VPN-вызов завис/упал ($e) — продолжаю');
}
logger.i(
'Connection: открываю сокет $host:$port '
'(прокси: ${proxySettings.isEnabled ? proxySettings.type.name : 'нет'})',
);
final socket = await _openSecureSocket(
host,
port,
@@ -109,7 +132,9 @@ class Connection {
socket = await connector.connect(host, port).timeout(connectTimeout);
logger.i('Подключено через прокси ${proxySettings.type.name}');
} else {
logger.i('Connection: TCP connect $host:$port (лимит ${connectTimeout.inSeconds}с)');
socket = await Socket.connect(host, port, timeout: connectTimeout);
logger.i('Connection: TCP установлен, начинаю TLS');
}
final allowInsecure = await TlsConfig.isInsecureAllowed();
if (allowInsecure) {
@@ -121,8 +146,11 @@ class Connection {
? SecureSocket.secure(socket, host: host, onBadCertificate: (_) => true)
: SecureSocket.secure(socket, host: host);
try {
return await secured.timeout(connectTimeout);
final result = await secured.timeout(connectTimeout);
logger.i('Connection: TLS-handshake завершён');
return result;
} on TimeoutException {
logger.w('Connection: TLS-handshake таймаут ${connectTimeout.inSeconds}с');
socket.destroy();
rethrow;
}
+58 -12
View File
@@ -57,11 +57,15 @@ class _SessionData {
final DateTime startedAt;
final List<_LogEntry> entries;
final bool truncated;
final List<String> logLines;
final bool logsTruncated;
_SessionData({
required this.startedAt,
required this.entries,
this.truncated = false,
this.logLines = const [],
this.logsTruncated = false,
});
static _SessionData fromJson(Map data) {
@@ -72,37 +76,43 @@ class _SessionData {
if (e is Map) entries.add(_LogEntry.fromJson(e));
}
}
final rawLogs = data['logs'];
final logLines = <String>[];
if (rawLogs is List) {
for (final l in rawLogs) {
logLines.add(l.toString());
}
}
return _SessionData(
startedAt:
DateTime.tryParse(data['startedAt']?.toString() ?? '') ??
DateTime.fromMillisecondsSinceEpoch(0),
entries: entries,
truncated: data['truncated'] == true,
logLines: logLines,
logsTruncated: data['logsTruncated'] == true,
);
}
}
/// Персистентный лог запросов для кнопки «Отладочный лог».
///
/// Хранит ВСЕ запросы (с payload) за последние [_maxSessions] заходов в
/// приложение: каждый запуск — отдельная сессия-файл, старые ротируются.
/// Пинги идут мимо [Api.sendRequest], поэтому в лог не попадают.
///
/// Payload сохраняется уже отредактированным — токен скрыт целиком, от номера
/// остаются первые 3 символа, остальное видно. Поэтому секреты не лежат на диске.
class DebugSessionLog {
DebugSessionLog._();
static final DebugSessionLog instance = DebugSessionLog._();
static const int _maxSessions = 3;
static const int _maxEntriesPerSession = 2000;
static const int _maxLogLinesPerSession = 5000;
static const Duration _flushDebounce = Duration(seconds: 3);
static final RegExp _ansiEscape = RegExp(r'\x1B\[[0-9;]*m');
Directory? _dir;
File? _currentFile;
DateTime? _currentStart;
final List<_LogEntry> _entries = [];
final List<String> _logLines = [];
bool _truncated = false;
bool _logsTruncated = false;
bool _initialized = false;
bool _dirty = false;
Timer? _flushTimer;
@@ -120,12 +130,23 @@ class DebugSessionLog {
_currentFile = File(
'${dir.path}/session_${_currentStart!.millisecondsSinceEpoch}.json',
);
if (_dirty) _scheduleFlush();
} catch (_) {
_dir = null;
_currentFile = null;
}
}
void recordLogLine(String line) {
final clean = line.replaceAll(_ansiEscape, '');
_logLines.add(clean);
if (_logLines.length > _maxLogLinesPerSession) {
_logLines.removeRange(0, _logLines.length - _maxLogLinesPerSession);
_logsTruncated = true;
}
_scheduleFlush();
}
void recordRequest(int opcode, int seq, dynamic payload) {
_entries.add(
_LogEntry(
@@ -189,6 +210,8 @@ class DebugSessionLog {
'startedAt': _currentStart?.toIso8601String(),
'truncated': _truncated,
'entries': _entries.map((e) => e.toJson()).toList(),
'logsTruncated': _logsTruncated,
'logs': List.of(_logLines),
});
await file.writeAsString(data);
} catch (_) {}
@@ -249,6 +272,8 @@ class DebugSessionLog {
startedAt: _currentStart ?? DateTime.now(),
entries: List.of(_entries),
truncated: _truncated,
logLines: List.of(_logLines),
logsTruncated: _logsTruncated,
),
);
sessions.sort((a, b) => a.startedAt.compareTo(b.startedAt));
@@ -256,7 +281,8 @@ class DebugSessionLog {
? sessions.sublist(sessions.length - _maxSessions)
: sessions;
final totalEntries = lastN.fold<int>(0, (sum, s) => sum + s.entries.length);
if (totalEntries == 0) return null;
final totalLogs = lastN.fold<int>(0, (sum, s) => sum + s.logLines.length);
if (totalEntries == 0 && totalLogs == 0) return null;
final buffer = StringBuffer();
buffer.writeln('Komet — отладочный лог');
@@ -264,6 +290,7 @@ class DebugSessionLog {
buffer.writeln('Экспортирован: ${DateTime.now().toIso8601String()}');
buffer.writeln('Заходов в приложение: ${lastN.length}');
buffer.writeln('Всего запросов: $totalEntries');
buffer.writeln('Всего строк лога: $totalLogs');
buffer.writeln('Скрыто: токен полностью, номер кроме первых 3 символов');
buffer.writeln();
@@ -275,12 +302,31 @@ class DebugSessionLog {
);
buffer.writeln(
'запросов: ${session.entries.length}'
'${session.truncated ? ' (обрезано до $_maxEntriesPerSession)' : ''}',
'${session.truncated ? ' (обрезано до $_maxEntriesPerSession)' : ''}'
' · строк лога: ${session.logLines.length}'
'${session.logsTruncated ? ' (обрезано до $_maxLogLinesPerSession)' : ''}',
);
buffer.writeln('==================================================');
buffer.writeln();
for (var i = 0; i < session.entries.length; i++) {
_writeEntry(buffer, i + 1, session.entries[i]);
buffer.writeln('----- ЛОГИ ПРИЛОЖЕНИЯ -----');
if (session.logLines.isEmpty) {
buffer.writeln('(пусто)');
} else {
for (final line in session.logLines) {
buffer.writeln(line);
}
}
buffer.writeln();
buffer.writeln('----- ЗАПРОСЫ -----');
if (session.entries.isEmpty) {
buffer.writeln('(пусто)');
buffer.writeln();
} else {
for (var i = 0; i < session.entries.length; i++) {
_writeEntry(buffer, i + 1, session.entries[i]);
}
}
}
return buffer.toString();
+12 -1
View File
@@ -3,6 +3,8 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import 'debug_session_log.dart';
Level _minimumLogLevel() {
const raw = String.fromEnvironment('KOMET_LOG_LEVEL', defaultValue: '');
switch (raw.toLowerCase()) {
@@ -41,9 +43,18 @@ final logger = Logger(
filter: _logFilter(),
level: _minimumLogLevel(),
printer: KometLogPrinter(),
output: ConsoleOutput(),
output: MultiOutput([ConsoleOutput(), DebugSessionLogOutput()]),
);
class DebugSessionLogOutput extends LogOutput {
@override
void output(OutputEvent event) {
for (final line in event.lines) {
DebugSessionLog.instance.recordLogLine(line);
}
}
}
int _importanceSortKey(Level level) {
final v = level.value;
if (v >= 5999) {