frontend polish and security review changes
This commit is contained in:
1 parent
5642b0b811
commit
71468f7594
13 files changed
+226
-227
No files matched your search
@@ -6,6 +6,7 @@ import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||
import 'package:revoltvpn/logic/crypto_service.dart';
|
||||
import 'package:revoltvpn/logic/hivemind_service.dart';
|
||||
import 'package:revoltvpn/logic/app_config.dart';
|
||||
import 'package:revoltvpn/logic/consent_manager.dart';
|
||||
|
||||
class AdManager extends ChangeNotifier {
|
||||
static const bool adsEnabled = false;
|
||||
@@ -27,8 +28,29 @@ class AdManager extends ChangeNotifier {
|
||||
if (adsEnabled) preloadAd();
|
||||
}
|
||||
|
||||
static Future<void>? _sdkInit;
|
||||
static Future<void> ensureSdkInitialized() {
|
||||
if (!adsEnabled) return Future.value();
|
||||
return _sdkInit ??= _initSdk();
|
||||
}
|
||||
|
||||
static Future<void> _initSdk() async {
|
||||
try {
|
||||
await ConsentManager.requestConsentIfNeeded()
|
||||
.timeout(const Duration(seconds: 5));
|
||||
} catch (e) {
|
||||
debugPrint('[AdManager] Consent init skipped: $e');
|
||||
}
|
||||
try {
|
||||
await MobileAds.instance.initialize().timeout(const Duration(seconds: 5));
|
||||
} catch (e) {
|
||||
debugPrint('[AdManager] MobileAds init skipped: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> preloadAd() async {
|
||||
if (!adsEnabled) return true;
|
||||
await ensureSdkInitialized();
|
||||
if (_isAdLoaded) return true;
|
||||
if (_isAdLoading) return _loadCompleter?.future ?? Future.value(false);
|
||||
|
||||
@@ -101,6 +123,8 @@ class AdManager extends ChangeNotifier {
|
||||
|
||||
if (!adsEnabled) return false;
|
||||
|
||||
await ensureSdkInitialized();
|
||||
|
||||
if (!_isAdLoaded || _rewardedAd == null) {
|
||||
final loaded = await preloadAd();
|
||||
if (!loaded || _rewardedAd == null) {
|
||||
|
||||
@@ -9,9 +9,6 @@ abstract final class AppConfig {
|
||||
/// Your server's public IP. The VPN tunnel is pinned to this.
|
||||
static const String serverIp = '0.0.0.0'; // ◄── REPLACE
|
||||
|
||||
/// API base — routed through the tunnel to Flask via Xray "api" outbound.
|
||||
static const String hivemindApiBase = 'http://10.254.254.1:5000/api';
|
||||
|
||||
// ── Bootstrap (first-connect config fetch) ────────────────────────────
|
||||
/// Hardcoded Reality config. App connects with this, fetches the real
|
||||
/// per-session VLESS URL through the tunnel, then reconnects.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
@@ -18,66 +17,6 @@ class HivemindService {
|
||||
return http.get(uri, headers: {'User-Agent': _ua}).timeout(timeout);
|
||||
}
|
||||
|
||||
static Future<http.Response> directPost(Uri uri, String body, {Duration timeout = const Duration(seconds: 5)}) {
|
||||
return http.post(uri, headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': _ua,
|
||||
}, body: body).timeout(timeout);
|
||||
}
|
||||
|
||||
static Future<http.Response> proxyGet(Uri uri, {Duration timeout = const Duration(seconds: 5)}) {
|
||||
return _proxyRequest('GET', uri, timeout: timeout);
|
||||
}
|
||||
|
||||
static Future<http.Response> proxyPost(Uri uri, String body, {Duration timeout = const Duration(seconds: 5)}) {
|
||||
return _proxyRequest('POST', uri, body: body, timeout: timeout);
|
||||
}
|
||||
|
||||
static Future<http.Response> _proxyRequest(String method, Uri uri,
|
||||
{String? body, Duration timeout = const Duration(seconds: 5)}) async {
|
||||
final socket = await Socket.connect('127.0.0.1', 10808, timeout: timeout);
|
||||
try {
|
||||
final target = uri.toString();
|
||||
final headers = <String, String>{
|
||||
'Host': '${uri.host}:${uri.port}',
|
||||
'User-Agent': _ua,
|
||||
'Accept': '*/*',
|
||||
'Connection': 'close',
|
||||
};
|
||||
if (body != null) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
headers['Content-Length'] = body.length.toString();
|
||||
}
|
||||
|
||||
final reqBuf = StringBuffer();
|
||||
reqBuf.write('$method $target HTTP/1.1\r\n');
|
||||
headers.forEach((k, v) => reqBuf.write('$k: $v\r\n'));
|
||||
reqBuf.write('\r\n');
|
||||
if (body != null) reqBuf.write(body);
|
||||
|
||||
socket.write(reqBuf.toString());
|
||||
await socket.flush();
|
||||
|
||||
final response = await socket.fold<String>(
|
||||
'',
|
||||
(prev, chunk) => prev + String.fromCharCodes(chunk),
|
||||
).timeout(timeout);
|
||||
|
||||
final parts = response.split('\r\n\r\n');
|
||||
if (parts.length < 2) throw Exception('Invalid proxy response');
|
||||
final headerBlock = parts[0];
|
||||
final respBody = parts.sublist(1).join('\r\n\r\n');
|
||||
|
||||
final headerLines = headerBlock.split('\r\n');
|
||||
final statusParts = headerLines[0].split(' ');
|
||||
final statusCode = int.parse(statusParts[1]);
|
||||
|
||||
return http.Response(respBody, statusCode);
|
||||
} finally {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
static void cancel() {
|
||||
_currentCallId++;
|
||||
}
|
||||
@@ -166,28 +105,8 @@ class HivemindService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> verifySession() async {
|
||||
try {
|
||||
final deviceId = await CryptoService.getDeviceId();
|
||||
final url = _tunnelUrl('/session/status?device_id=$deviceId');
|
||||
final response = await proxyGet(url);
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return data['active'] == true;
|
||||
}
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Idle sessions are culled server-side by the management loop
|
||||
// after 2 minutes of no /status polls (user disconnected).
|
||||
static void disconnectAndCleanup() {}
|
||||
|
||||
// ── URL builders ──────────────────────────────────────────────────
|
||||
|
||||
static Uri _publicUrl(String path) =>
|
||||
Uri.parse('${AppConfig.hivemindApiPublic}$path');
|
||||
|
||||
static Uri _tunnelUrl(String path) =>
|
||||
Uri.parse('${AppConfig.hivemindApiBase}$path');
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_vless/flutter_vless.dart';
|
||||
import 'package:revoltvpn/logic/hivemind_service.dart';
|
||||
@@ -33,12 +32,24 @@ class VpnConnection extends ChangeNotifier {
|
||||
|
||||
late final FlutterVless _vless;
|
||||
bool _initialized = false;
|
||||
final Completer<void> _readyCompleter = Completer<void>();
|
||||
Future<void> get ready => _readyCompleter.future;
|
||||
|
||||
VpnConnection() {
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
try {
|
||||
await _startEngine();
|
||||
} catch (e) {
|
||||
debugPrint('[VPN] Engine init failed: $e');
|
||||
} finally {
|
||||
if (!_readyCompleter.isCompleted) _readyCompleter.complete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startEngine() async {
|
||||
if (kIsWeb) return;
|
||||
|
||||
_vless = FlutterVless(
|
||||
@@ -177,22 +188,10 @@ class VpnConnection extends ChangeNotifier {
|
||||
|
||||
try {
|
||||
final parsed = FlutterVless.parse(realUrl);
|
||||
final configJson =
|
||||
jsonDecode(parsed.getFullConfiguration()) as Map<String, dynamic>;
|
||||
|
||||
// Inject HTTP inbound for tunnel-internal API calls.
|
||||
final inbounds = (configJson['inbounds'] as List?) ?? [];
|
||||
inbounds.add({
|
||||
'tag': 'http-proxy',
|
||||
'port': 10808,
|
||||
'listen': '127.0.0.1',
|
||||
'protocol': 'http',
|
||||
});
|
||||
configJson['inbounds'] = inbounds;
|
||||
|
||||
await _vless.startVless(
|
||||
remark: parsed.remark.isNotEmpty ? parsed.remark : 'Revolt VPN',
|
||||
config: jsonEncode(configJson),
|
||||
config: parsed.getFullConfiguration(),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[VPN] Tunnel start error: $e');
|
||||
|
||||
Reference in new issue
Block a user