diff --git a/lib/logic/hivemind_service.dart b/lib/logic/hivemind_service.dart index 007bd71..81111df 100644 --- a/lib/logic/hivemind_service.dart +++ b/lib/logic/hivemind_service.dart @@ -76,6 +76,71 @@ class HivemindService { } } + static Future controlPost( + Uri uri, + Map body, { + Duration timeout = const Duration(seconds: 5), + }) async { + final client = http.Client(); + final request = http.Request('POST', uri) + ..headers['User-Agent'] = _ua + ..headers['Content-Type'] = 'application/json' + ..body = jsonEncode(body) + ..followRedirects = false; + + Future send() async { + final streamed = await client.send(request); + final declaredLength = streamed.contentLength; + if (declaredLength != null && declaredLength > _maxControlResponseBytes) { + throw const FormatException('Control response is too large.'); + } + + final bytes = []; + await for (final chunk in streamed.stream) { + if (bytes.length + chunk.length > _maxControlResponseBytes) { + throw const FormatException('Control response is too large.'); + } + bytes.addAll(chunk); + } + + return http.Response.bytes(bytes, streamed.statusCode, + headers: streamed.headers, request: request); + } + + try { + return await send().timeout( + timeout, + onTimeout: () { + client.close(); + throw TimeoutException('Control request timed out.', timeout); + }, + ); + } finally { + client.close(); + } + } + + // Hands the session back so the server can drop the VLESS client instead of + // leaving it provisioned until the window runs out. + // + // Only the user-initiated disconnect calls this. An expiry, a hard cap or a + // server-ended session is already gone server-side, and /session/stop takes + // nothing but a device_id as proof (hivemind/api.py notes this), so a + // transient client error must never be allowed to destroy a live session. + // Best effort throughout: the session expires on its own regardless. + static Future releaseSession() async { + try { + final deviceId = await CryptoService.getDeviceId(); + await controlPost( + _publicUrl('/session/stop'), + {'device_id': deviceId}, + timeout: const Duration(seconds: 3), + ); + } catch (_) { + // Nothing to recover: the server expires the session anyway. + } + } + static void cancel() { _currentCallId++; _expectedNonce = null; diff --git a/lib/logic/session_timer.dart b/lib/logic/session_timer.dart index d55ee1c..44e54e7 100644 --- a/lib/logic/session_timer.dart +++ b/lib/logic/session_timer.dart @@ -241,6 +241,10 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { } Future disconnect({String reason = 'User requested'}) async { + // Only here, not in _doDisconnect: this is the one path the user actually + // asked for. Unawaited so a slow or unreachable API never holds up the + // teardown. + unawaited(HivemindService.releaseSession()); await _doDisconnect(reason); }