deleting old safety checks and connect polish
This commit is contained in:
1 parent
a863656758
commit
a32252c56d
3 files changed
+55
-64
No files matched your search
@@ -7,6 +7,13 @@ import 'package:revoltvpn/logic/session_timer.dart';
|
|||||||
import 'package:revoltvpn/logic/ad_manager.dart';
|
import 'package:revoltvpn/logic/ad_manager.dart';
|
||||||
|
|
||||||
/// ConnectButton — circle with brand.png + pulsing glow when connected.
|
/// ConnectButton — circle with brand.png + pulsing glow when connected.
|
||||||
|
///
|
||||||
|
/// Rules:
|
||||||
|
/// disconnected → connect (ad → config → tunnel)
|
||||||
|
/// connecting → disconnect (cancel)
|
||||||
|
/// connected → disconnect
|
||||||
|
/// error → connect (retry)
|
||||||
|
/// disconnecting → ignore
|
||||||
class ConnectButton extends StatefulWidget {
|
class ConnectButton extends StatefulWidget {
|
||||||
const ConnectButton({super.key});
|
const ConnectButton({super.key});
|
||||||
|
|
||||||
@@ -17,6 +24,7 @@ class ConnectButton extends StatefulWidget {
|
|||||||
class _ConnectButtonState extends State<ConnectButton>
|
class _ConnectButtonState extends State<ConnectButton>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
bool _busy = false;
|
bool _busy = false;
|
||||||
|
DateTime _lastTap = DateTime.now();
|
||||||
AnimationController? _pulse;
|
AnimationController? _pulse;
|
||||||
Animation<double>? _pulseAnim;
|
Animation<double>? _pulseAnim;
|
||||||
|
|
||||||
@@ -51,21 +59,30 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleTap() async {
|
Future<void> _handleTap() async {
|
||||||
|
// 1-second cooldown to prevent spam
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (now.difference(_lastTap).inMilliseconds < 1000) return;
|
||||||
|
_lastTap = now;
|
||||||
|
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
|
|
||||||
final vpn = context.read<VpnConnection>();
|
final vpn = context.read<VpnConnection>();
|
||||||
final timer = context.read<SessionTimer>();
|
final timer = context.read<SessionTimer>();
|
||||||
final ad = context.read<AdManager>();
|
final ad = context.read<AdManager>();
|
||||||
|
|
||||||
// Disconnect always works — even mid-connection
|
// ── Disconnect if connected or connecting ───────────────────────────
|
||||||
if (vpn.status == VpnStatus.connected || vpn.status == VpnStatus.connecting) {
|
if (vpn.status == VpnStatus.connected || vpn.status == VpnStatus.connecting) {
|
||||||
_busy = false; // cancel any in-flight connect
|
_busy = false;
|
||||||
|
timer.stop(); // MUST stop timer BEFORE disconnect — prevents auto-reconnect
|
||||||
await vpn.disconnect();
|
await vpn.disconnect();
|
||||||
timer.stop();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_busy) return; // already connecting, prevent double-tap
|
// ── Ignore if disconnecting ─────────────────────────────────────────
|
||||||
|
if (vpn.status == VpnStatus.disconnecting) return;
|
||||||
|
|
||||||
|
// ── Connect (or retry) ─────────────────────────────────────────────
|
||||||
|
if (_busy) return;
|
||||||
|
|
||||||
setState(() => _busy = true);
|
setState(() => _busy = true);
|
||||||
try {
|
try {
|
||||||
@@ -87,8 +104,7 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
builder: (context, vpn, _) {
|
builder: (context, vpn, _) {
|
||||||
final isConnected = vpn.status == VpnStatus.connected;
|
final isConnected = vpn.status == VpnStatus.connected;
|
||||||
final spinning = vpn.status == VpnStatus.connecting ||
|
final spinning = vpn.status == VpnStatus.connecting ||
|
||||||
vpn.status == VpnStatus.disconnecting ||
|
vpn.status == VpnStatus.disconnecting;
|
||||||
_busy;
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: _handleTap,
|
onTap: _handleTap,
|
||||||
@@ -130,7 +146,7 @@ class _ConnectButtonState extends State<ConnectButton>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.all(0), // <-- CHANGE ME: brand.png padding from edge
|
padding: const EdgeInsets.all(14), // <-- CHANGE ME: brand.png padding from edge
|
||||||
child: Image.asset('assets/brand.png', fit: BoxFit.contain),
|
child: Image.asset('assets/brand.png', fit: BoxFit.contain),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,104 +7,82 @@ import 'package:revoltvpn/logic/app_config.dart';
|
|||||||
import 'package:revoltvpn/logic/crypto_service.dart';
|
import 'package:revoltvpn/logic/crypto_service.dart';
|
||||||
|
|
||||||
class HivemindService {
|
class HivemindService {
|
||||||
/// Nonce generated at the start of a connect cycle.
|
|
||||||
/// Sent to the server during session creation and verified in every poll
|
|
||||||
/// response so we never accidentally resume a stale session.
|
|
||||||
static String? _expectedNonce;
|
static String? _expectedNonce;
|
||||||
|
static int _currentCallId = 0;
|
||||||
|
|
||||||
|
/// Call this from disconnect to abort any in-progress polling.
|
||||||
|
static void cancel() {
|
||||||
|
_currentCallId++; // bump — any running loop sees a stale ID and exits
|
||||||
|
}
|
||||||
|
|
||||||
/// Stores a nonce so that fetchConfigWithPolling can verify it.
|
|
||||||
/// Called by AdManager.showAd() before showing a rewarded ad so the
|
|
||||||
/// subsequent poll accepts only the session created by *this* ad.
|
|
||||||
static void setExpectedNonce(String nonce) {
|
static void setExpectedNonce(String nonce) {
|
||||||
_expectedNonce = nonce;
|
_expectedNonce = nonce;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Polls the Hivemind server until the session is ready or timeout occurs.
|
/// Tries to fetch the VLESS config from the server.
|
||||||
/// Returns a VLESS connection URL string.
|
/// Only polls a few times — by the time the ad finishes, the SSV callback
|
||||||
///
|
/// has usually already fired.
|
||||||
/// [onAttempt] is called after each poll with (current, total) so the UI
|
|
||||||
/// can show progress (e.g. "Contacting server (3/15)…").
|
|
||||||
|
|
||||||
static Future<String> fetchConfigWithPolling({
|
static Future<String> fetchConfigWithPolling({
|
||||||
void Function(int attempt, int total)? onAttempt,
|
void Function(int attempt, int total)? onAttempt,
|
||||||
}) async {
|
}) async {
|
||||||
final deviceId = await CryptoService.getDeviceId();
|
final deviceId = await CryptoService.getDeviceId();
|
||||||
|
|
||||||
// ── Generate a connect nonce ──────────────────────────────────────
|
final callId = ++_currentCallId;
|
||||||
final nonce = '${Random().nextInt(0x7FFFFFFF)}-${DateTime.now().millisecondsSinceEpoch}';
|
final nonce = '${Random().nextInt(0x7FFFFFFF)}-${DateTime.now().millisecondsSinceEpoch}';
|
||||||
_expectedNonce = nonce;
|
_expectedNonce = nonce;
|
||||||
debugPrint('[HivemindService] Connect nonce: $nonce');
|
debugPrint('[HivemindService] Call #$callId — nonce: $nonce');
|
||||||
|
|
||||||
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
|
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
|
||||||
|
|
||||||
// ── AD BYPASS (debug only) ────────────────────────────────────────
|
// ── AD BYPASS (debug only) ────────────────────────────────────────
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
try {
|
try {
|
||||||
final customData = jsonEncode({
|
final customData = jsonEncode({'device_id': deviceId, 'nonce': nonce});
|
||||||
'device_id': deviceId,
|
final fakeUrl = Uri.parse(
|
||||||
'nonce': nonce,
|
|
||||||
});
|
|
||||||
final fakeAdmobPingUrl = Uri.parse(
|
|
||||||
'${AppConfig.hivemindApiBase}/admob/callback?signature=test&key_id=test&custom_data=${Uri.encodeComponent(customData)}');
|
'${AppConfig.hivemindApiBase}/admob/callback?signature=test&key_id=test&custom_data=${Uri.encodeComponent(customData)}');
|
||||||
await http.get(fakeAdmobPingUrl).timeout(const Duration(seconds: 8));
|
await http.get(fakeUrl).timeout(const Duration(seconds: 8));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
// ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
int attempts = 0;
|
const maxAttempts = 5;
|
||||||
while (attempts < 15) {
|
for (int i = 1; i <= maxAttempts; i++) {
|
||||||
debugPrint('[HivemindService] Polling attempt ${attempts + 1}/15…');
|
if (_currentCallId != callId) throw Exception('Cancelled');
|
||||||
onAttempt?.call(attempts + 1, 15);
|
|
||||||
|
onAttempt?.call(i, maxAttempts);
|
||||||
try {
|
try {
|
||||||
final response = await http.get(url).timeout(const Duration(seconds: 10));
|
final response = await http.get(url).timeout(const Duration(seconds: 5));
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
|
|
||||||
debugPrint('[HivemindService] Session found, checking nonce…');
|
|
||||||
|
|
||||||
// ── Nonce gate ──────────────────────────────────────────────
|
|
||||||
final serverNonce = data['nonce'] as String?;
|
final serverNonce = data['nonce'] as String?;
|
||||||
if (_expectedNonce != null && serverNonce != null && serverNonce != _expectedNonce) {
|
if (_expectedNonce != null && serverNonce != null && serverNonce != _expectedNonce) {
|
||||||
debugPrint('[HivemindService] Nonce mismatch (expected $_expectedNonce, got $serverNonce) — stale session, retrying…');
|
debugPrint('[HivemindService] Nonce mismatch — retrying…');
|
||||||
} else if (data['active'] == true && data['vless_uuid'] != null) {
|
} else if (data['active'] == true && data['vless_uuid'] != null) {
|
||||||
final vlessUuid = data['vless_uuid'];
|
final vlessUuid = data['vless_uuid'];
|
||||||
|
|
||||||
// Build VLESS connection URL
|
|
||||||
final vlessUrl = 'vless://$vlessUuid@${AppConfig.serverDomain}:${AppConfig.serverPort}'
|
final vlessUrl = 'vless://$vlessUuid@${AppConfig.serverDomain}:${AppConfig.serverPort}'
|
||||||
'?path=${Uri.encodeComponent(AppConfig.vlessPath)}'
|
'?path=${Uri.encodeComponent(AppConfig.vlessPath)}'
|
||||||
'&security=${AppConfig.vlessSecurity}'
|
'&security=${AppConfig.vlessSecurity}'
|
||||||
'&type=${AppConfig.vlessType}'
|
'&type=${AppConfig.vlessType}'
|
||||||
'#ReVoltVPN';
|
'#ReVoltVPN';
|
||||||
|
_expectedNonce = null;
|
||||||
_expectedNonce = null; // consumed successfully
|
|
||||||
return vlessUrl;
|
return vlessUrl;
|
||||||
} else if (data['active'] == true && data['client_ip'] != null) {
|
|
||||||
// Server is still running AWG — VLESS client cannot use this.
|
|
||||||
debugPrint('[HivemindService] Server returned AWG format (client_ip) — server migration needed.');
|
|
||||||
throw Exception('Server is running the old AWG protocol. '
|
|
||||||
'Please update the server to VLESS.');
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
debugPrint('[HivemindService] Non-200 status: ${response.statusCode}');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[HivemindService] Polling exception: $e');
|
if (e.toString().contains('Cancelled')) rethrow;
|
||||||
// Ignore network errors and keep polling
|
debugPrint('[HivemindService] Attempt $i failed: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
attempts++;
|
if (i < maxAttempts) await Future.delayed(const Duration(seconds: 1));
|
||||||
await Future.delayed(const Duration(seconds: 2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Exception("Session not activated. S2S callback failed or timed out.");
|
throw Exception('Session not activated. Server callback may have timed out.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quick one-shot check: does the server have a live session for this device?
|
|
||||||
static Future<bool> verifySession() async {
|
static Future<bool> verifySession() async {
|
||||||
try {
|
try {
|
||||||
final deviceId = await CryptoService.getDeviceId();
|
final deviceId = await CryptoService.getDeviceId();
|
||||||
final url = Uri.parse(
|
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
|
||||||
'${AppConfig.hivemindApiBase}/session/status?device_id=$deviceId');
|
|
||||||
final response = await http.get(url).timeout(const Duration(seconds: 5));
|
final response = await http.get(url).timeout(const Duration(seconds: 5));
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(response.body);
|
final data = jsonDecode(response.body);
|
||||||
@@ -114,7 +92,6 @@ class HivemindService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disconnects the session (cleans up on server)
|
|
||||||
static Future<void> disconnectAndCleanup() async {
|
static Future<void> disconnectAndCleanup() async {
|
||||||
final deviceId = await CryptoService.getDeviceId();
|
final deviceId = await CryptoService.getDeviceId();
|
||||||
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/stop');
|
final url = Uri.parse('${AppConfig.hivemindApiBase}/session/stop');
|
||||||
@@ -124,9 +101,6 @@ class HivemindService {
|
|||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: jsonEncode({'device_id': deviceId}),
|
body: jsonEncode({'device_id': deviceId}),
|
||||||
).timeout(const Duration(seconds: 3));
|
).timeout(const Duration(seconds: 3));
|
||||||
} catch (_) {
|
} catch (_) {}
|
||||||
// Best effort cleanup
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -132,9 +132,9 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[VPN] Hivemind config error: $e');
|
debugPrint('[VPN] Hivemind config error: $e');
|
||||||
final raw = e.toString().replaceAll('Exception: ', '');
|
final raw = e.toString().replaceAll('Exception: ', '');
|
||||||
if (raw.contains('AWG protocol') || raw.contains('old AWG')) {
|
if (raw.contains('Cancelled')) {
|
||||||
_errorMessage = 'Server update required.\nThe VPN server needs to be upgraded to VLESS.';
|
// User tapped disconnect — silently abort
|
||||||
_setStatus(VpnStatus.error, 'Server outdated');
|
return false;
|
||||||
} else if (raw.contains('timed out') || raw.contains('Session not activated')) {
|
} else if (raw.contains('timed out') || raw.contains('Session not activated')) {
|
||||||
_errorMessage = 'The server did not respond in time.\nCheck your connection and try again.';
|
_errorMessage = 'The server did not respond in time.\nCheck your connection and try again.';
|
||||||
_setStatus(VpnStatus.error, 'Server unreachable');
|
_setStatus(VpnStatus.error, 'Server unreachable');
|
||||||
@@ -202,6 +202,7 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
// ── Disconnect ────────────────────────────────────────────────────────────
|
// ── Disconnect ────────────────────────────────────────────────────────────
|
||||||
Future<void> disconnect() async {
|
Future<void> disconnect() async {
|
||||||
_cancelled = true;
|
_cancelled = true;
|
||||||
|
HivemindService.cancel(); // abort any in-progress polling
|
||||||
if (_status == VpnStatus.disconnected || _status == VpnStatus.disconnecting) {
|
if (_status == VpnStatus.disconnected || _status == VpnStatus.disconnecting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user