initial
This commit is contained in:
@@ -212,10 +212,6 @@
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name="id.flutter.flutter_background_service.BackgroundService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
(previous, next) async {
|
||||
await ref
|
||||
.read(torProxyRepositoryProvider.notifier)
|
||||
.setProxyPort(next ?? -1);
|
||||
.setProxyPort(next?.socksPort ?? -1);
|
||||
},
|
||||
onError: (error, stackTrace) {
|
||||
logger.e(
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ final class ProxySettingsReplicationProvider
|
||||
}
|
||||
|
||||
String _$proxySettingsReplicationHash() =>
|
||||
r'9ded7af1d745e25c59e22edecea582e25230e5e7';
|
||||
r'e4c5e35b9aab2aae60e3f09a99472f98a7beb69e';
|
||||
|
||||
abstract class _$ProxySettingsReplication extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'moat.g.dart';
|
||||
|
||||
enum MoatTransportType {
|
||||
obfs4('obfs4'),
|
||||
snowflake('snowflake'),
|
||||
meek('meek'),
|
||||
meekAzure('meek-azure'),
|
||||
webtunnel('webtunnel');
|
||||
|
||||
const MoatTransportType(this.value);
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
static MoatTransportType? fromString(String value) {
|
||||
for (final transport in MoatTransportType.values) {
|
||||
if (transport.value == value) return transport;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class SettingsRequest {
|
||||
final String? country;
|
||||
@JsonKey(toJson: _transportsToJson, fromJson: _transportsFromJson)
|
||||
final List<MoatTransportType> transports;
|
||||
|
||||
const SettingsRequest({
|
||||
this.country,
|
||||
this.transports = const [
|
||||
MoatTransportType.obfs4,
|
||||
MoatTransportType.snowflake,
|
||||
MoatTransportType.webtunnel,
|
||||
],
|
||||
});
|
||||
|
||||
static List<String> _transportsToJson(List<MoatTransportType> transports) =>
|
||||
transports.map((t) => t.value).toList();
|
||||
|
||||
static List<MoatTransportType> _transportsFromJson(List<dynamic> json) => json
|
||||
.cast<String>()
|
||||
.map((s) => MoatTransportType.fromString(s))
|
||||
.where((t) => t != null)
|
||||
.cast<MoatTransportType>()
|
||||
.toList();
|
||||
|
||||
factory SettingsRequest.fromJson(Map<String, dynamic> json) =>
|
||||
_$SettingsRequestFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SettingsRequestToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class SettingsResponse {
|
||||
final List<Setting>? settings;
|
||||
final String? country;
|
||||
final List<MoatError>? errors;
|
||||
|
||||
const SettingsResponse({this.settings, this.country, this.errors});
|
||||
|
||||
factory SettingsResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$SettingsResponseFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SettingsResponseToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Setting {
|
||||
@JsonKey(name: 'bridges')
|
||||
final Bridge bridge;
|
||||
|
||||
const Setting({required this.bridge});
|
||||
|
||||
factory Setting.fromJson(Map<String, dynamic> json) =>
|
||||
_$SettingFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SettingToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Bridge {
|
||||
@JsonKey(toJson: _transportToJson, fromJson: _transportFromJson)
|
||||
final MoatTransportType type;
|
||||
final String source;
|
||||
@JsonKey(name: 'bridge_strings')
|
||||
final List<String>? bridges;
|
||||
|
||||
const Bridge({required this.type, required this.source, this.bridges});
|
||||
|
||||
static String _transportToJson(MoatTransportType transport) =>
|
||||
transport.value;
|
||||
|
||||
static MoatTransportType _transportFromJson(dynamic json) =>
|
||||
MoatTransportType.fromString(json as String)!;
|
||||
|
||||
factory Bridge.fromJson(Map<String, dynamic> json) => _$BridgeFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$BridgeToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class MoatError implements Exception {
|
||||
final String? id;
|
||||
final String? type;
|
||||
final String? version;
|
||||
final int? code;
|
||||
final String? status;
|
||||
final String? detail;
|
||||
|
||||
const MoatError({
|
||||
this.id,
|
||||
this.type,
|
||||
this.version,
|
||||
this.code,
|
||||
this.status,
|
||||
this.detail,
|
||||
});
|
||||
|
||||
factory MoatError.fromJson(Map<String, dynamic> json) =>
|
||||
_$MoatErrorFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$MoatErrorToJson(this);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (detail != null && detail!.isNotEmpty) {
|
||||
return detail!;
|
||||
}
|
||||
return '$code $status';
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class BuiltInBridges {
|
||||
final List<String> meek;
|
||||
|
||||
@JsonKey(name: 'meek-azure')
|
||||
final List<String> meekAzure;
|
||||
|
||||
final List<String> obfs4;
|
||||
final List<String> snowflake;
|
||||
|
||||
const BuiltInBridges({
|
||||
required this.meek,
|
||||
required this.meekAzure,
|
||||
required this.obfs4,
|
||||
required this.snowflake,
|
||||
});
|
||||
|
||||
factory BuiltInBridges.fromJson(Map<String, dynamic> json) =>
|
||||
_$BuiltInBridgesFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$BuiltInBridgesToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'moat.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SettingsRequest _$SettingsRequestFromJson(Map<String, dynamic> json) =>
|
||||
SettingsRequest(
|
||||
country: json['country'] as String?,
|
||||
transports: json['transports'] == null
|
||||
? const [
|
||||
MoatTransportType.obfs4,
|
||||
MoatTransportType.snowflake,
|
||||
MoatTransportType.webtunnel,
|
||||
]
|
||||
: SettingsRequest._transportsFromJson(json['transports'] as List),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SettingsRequestToJson(SettingsRequest instance) =>
|
||||
<String, dynamic>{
|
||||
'country': instance.country,
|
||||
'transports': SettingsRequest._transportsToJson(instance.transports),
|
||||
};
|
||||
|
||||
SettingsResponse _$SettingsResponseFromJson(Map<String, dynamic> json) =>
|
||||
SettingsResponse(
|
||||
settings: (json['settings'] as List<dynamic>?)
|
||||
?.map((e) => Setting.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
country: json['country'] as String?,
|
||||
errors: (json['errors'] as List<dynamic>?)
|
||||
?.map((e) => MoatError.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SettingsResponseToJson(SettingsResponse instance) =>
|
||||
<String, dynamic>{
|
||||
'settings': instance.settings?.map((e) => e.toJson()).toList(),
|
||||
'country': instance.country,
|
||||
'errors': instance.errors?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
Setting _$SettingFromJson(Map<String, dynamic> json) =>
|
||||
Setting(bridge: Bridge.fromJson(json['bridges'] as Map<String, dynamic>));
|
||||
|
||||
Map<String, dynamic> _$SettingToJson(Setting instance) => <String, dynamic>{
|
||||
'bridges': instance.bridge.toJson(),
|
||||
};
|
||||
|
||||
Bridge _$BridgeFromJson(Map<String, dynamic> json) => Bridge(
|
||||
type: Bridge._transportFromJson(json['type']),
|
||||
source: json['source'] as String,
|
||||
bridges: (json['bridge_strings'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BridgeToJson(Bridge instance) => <String, dynamic>{
|
||||
'type': Bridge._transportToJson(instance.type),
|
||||
'source': instance.source,
|
||||
'bridge_strings': instance.bridges,
|
||||
};
|
||||
|
||||
MoatError _$MoatErrorFromJson(Map<String, dynamic> json) => MoatError(
|
||||
id: json['id'] as String?,
|
||||
type: json['type'] as String?,
|
||||
version: json['version'] as String?,
|
||||
code: (json['code'] as num?)?.toInt(),
|
||||
status: json['status'] as String?,
|
||||
detail: json['detail'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MoatErrorToJson(MoatError instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'type': instance.type,
|
||||
'version': instance.version,
|
||||
'code': instance.code,
|
||||
'status': instance.status,
|
||||
'detail': instance.detail,
|
||||
};
|
||||
|
||||
BuiltInBridges _$BuiltInBridgesFromJson(Map<String, dynamic> json) =>
|
||||
BuiltInBridges(
|
||||
meek: (json['meek'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
meekAzure: (json['meek-azure'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
obfs4: (json['obfs4'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
snowflake: (json['snowflake'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BuiltInBridgesToJson(BuiltInBridges instance) =>
|
||||
<String, dynamic>{
|
||||
'meek': instance.meek,
|
||||
'meek-azure': instance.meekAzure,
|
||||
'obfs4': instance.obfs4,
|
||||
'snowflake': instance.snowflake,
|
||||
};
|
||||
@@ -23,8 +23,8 @@ import 'dart:io';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
import 'package:pluggable_transports_proxy/pluggable_transports_proxy.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/tor/data/models/moat.dart';
|
||||
|
||||
part 'builtin_bridges.g.dart';
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:weblibre/features/tor/data/models/moat.dart';
|
||||
|
||||
class MoatApi {
|
||||
static const String _baseUrl =
|
||||
'https://bridges.torproject.org/moat/circumvention/';
|
||||
|
||||
final http.Client _client;
|
||||
|
||||
MoatApi(this._client);
|
||||
|
||||
Future<SettingsResponse> settings([SettingsRequest? request]) async {
|
||||
final response = await _post(
|
||||
'settings',
|
||||
request ?? const SettingsRequest(),
|
||||
);
|
||||
return SettingsResponse.fromJson(
|
||||
jsonDecode(response.body) as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
Future<SettingsResponse> defaults([SettingsRequest? request]) async {
|
||||
final response = await _post(
|
||||
'defaults',
|
||||
request ?? const SettingsRequest(),
|
||||
);
|
||||
return SettingsResponse.fromJson(
|
||||
jsonDecode(response.body) as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Map<String, SettingsResponse>> map() async {
|
||||
final response = await _get('map');
|
||||
final Map<String, dynamic> data =
|
||||
jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return data.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
SettingsResponse.fromJson(value as Map<String, dynamic>),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<BuiltInBridges> builtin() async {
|
||||
final response = await _get('builtin');
|
||||
return BuiltInBridges.fromJson(
|
||||
jsonDecode(response.body) as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<String>> countries() async {
|
||||
final response = await _get('countries');
|
||||
return List<String>.from(jsonDecode(response.body) as List);
|
||||
}
|
||||
|
||||
Future<http.Response> _get(String endpoint) async {
|
||||
final uri = Uri.parse('$_baseUrl$endpoint');
|
||||
return await _client
|
||||
.get(uri, headers: _headers)
|
||||
.timeout(const Duration(seconds: 15));
|
||||
}
|
||||
|
||||
Future<http.Response> _post(String endpoint, Object body) async {
|
||||
final uri = Uri.parse('$_baseUrl$endpoint');
|
||||
return await _client
|
||||
.post(uri, headers: _headers, body: jsonEncode(body))
|
||||
.timeout(const Duration(seconds: 15));
|
||||
}
|
||||
|
||||
Map<String, String> get _headers => {
|
||||
'Content-Type': 'application/vnd.api+json',
|
||||
'Accept': 'application/vnd.api+json',
|
||||
};
|
||||
|
||||
void dispose() {
|
||||
_client.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_tor/flutter_tor.dart';
|
||||
import 'package:http/io_client.dart';
|
||||
import 'package:socks5_proxy/socks_client.dart';
|
||||
import 'package:weblibre/features/tor/data/models/moat.dart';
|
||||
import 'package:weblibre/features/tor/data/services/moat_api.dart';
|
||||
|
||||
const String _meekParameters =
|
||||
'url=https://1723079976.rsc.cdn77.org;front=www.phpmyadmin.net';
|
||||
|
||||
const defaultBridges = [MoatTransportType.obfs4, MoatTransportType.snowflake];
|
||||
|
||||
/// Manages MOAT API connections with persistent proxy setup.
|
||||
class MoatService {
|
||||
MoatApi? _api;
|
||||
|
||||
/// Initializes the MeekLite proxy and MOAT API connection.
|
||||
/// Must be called before using other methods.
|
||||
Future<void> initialize() async {
|
||||
if (_api != null) return;
|
||||
|
||||
final port = await IPtProxyController().start(TransportType.meek, "");
|
||||
|
||||
final httpClient = HttpClient();
|
||||
SocksTCPClient.assignToHttpClient(httpClient, [
|
||||
ProxySettings(
|
||||
InternetAddress.loopbackIPv4,
|
||||
port,
|
||||
username: _meekParameters,
|
||||
password: '\u0000',
|
||||
),
|
||||
]);
|
||||
|
||||
_api = MoatApi(IOClient(httpClient));
|
||||
}
|
||||
|
||||
/// Disposes the API connection and stops the MeekLite proxy.
|
||||
/// Should be called when done using the client.
|
||||
Future<void> dispose() async {
|
||||
if (_api == null) return;
|
||||
|
||||
_api!.dispose();
|
||||
await IPtProxyController().stop(TransportType.meek);
|
||||
_api = null;
|
||||
}
|
||||
|
||||
/// Ensures the client is initialized before API calls.
|
||||
void _ensureInitialized() {
|
||||
if (_api == null) {
|
||||
throw StateError(
|
||||
'MoatClient must be initialized before use. Call initialize() first.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles MOAT API errors and determines if PT is required.
|
||||
bool _handleMoatErrors(List<MoatError>? errors) {
|
||||
if (errors == null || errors.isEmpty) return false;
|
||||
|
||||
final error = errors.first;
|
||||
if (error.code == 404 || error.code == 406) {
|
||||
// 404: Needs transport, but not the available ones
|
||||
// 406: No country from IP address
|
||||
return true;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
/// Converts BuiltInBridges to Settings for requested transports.
|
||||
static List<Setting> convertBuiltinToSettings(
|
||||
BuiltInBridges builtinBridges, {
|
||||
List<MoatTransportType> transports = defaultBridges,
|
||||
}) {
|
||||
final settings = <Setting>[];
|
||||
|
||||
for (final transport in transports) {
|
||||
final bridgeStrings = switch (transport) {
|
||||
MoatTransportType.obfs4 => builtinBridges.obfs4,
|
||||
MoatTransportType.snowflake => builtinBridges.snowflake,
|
||||
MoatTransportType.meek => builtinBridges.meek,
|
||||
MoatTransportType.meekAzure => builtinBridges.meekAzure,
|
||||
MoatTransportType.webtunnel => <String>[], // Not available in builtin
|
||||
};
|
||||
|
||||
if (bridgeStrings.isNotEmpty) {
|
||||
settings.add(
|
||||
Setting(
|
||||
bridge: Bridge(
|
||||
type: transport,
|
||||
source: 'builtin',
|
||||
bridges: bridgeStrings,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// Gets built-in bridges from the MOAT service endpoint.
|
||||
Future<BuiltInBridges> getBuiltinBridges({
|
||||
List<MoatTransportType> transports = defaultBridges,
|
||||
}) async {
|
||||
_ensureInitialized();
|
||||
|
||||
final builtinBridges = await _api!.builtin();
|
||||
return builtinBridges;
|
||||
}
|
||||
|
||||
/// Tries to automatically configure Pluggable Transports.
|
||||
Future<List<Setting>?> autoConf({
|
||||
String? country,
|
||||
bool cannotConnectWithoutPt = false,
|
||||
List<MoatTransportType> transports = defaultBridges,
|
||||
}) async {
|
||||
_ensureInitialized();
|
||||
|
||||
bool localCannotConnectWithoutPt = cannotConnectWithoutPt;
|
||||
final request = SettingsRequest(country: country, transports: transports);
|
||||
|
||||
var response = await _api!.settings(request);
|
||||
|
||||
if (_handleMoatErrors(response.errors)) {
|
||||
localCannotConnectWithoutPt = true;
|
||||
}
|
||||
|
||||
final hasSettings = response.settings?.isNotEmpty ?? false;
|
||||
if (!hasSettings && !localCannotConnectWithoutPt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasSettings) {
|
||||
return response.settings;
|
||||
}
|
||||
|
||||
response = await _api!.defaults(SettingsRequest(transports: transports));
|
||||
return response.settings;
|
||||
}
|
||||
|
||||
Future<List<Setting>?> getDefaultBridges({
|
||||
List<MoatTransportType> transports = defaultBridges,
|
||||
}) async {
|
||||
_ensureInitialized();
|
||||
|
||||
final response = await _api!.defaults(
|
||||
SettingsRequest(transports: transports),
|
||||
);
|
||||
|
||||
return response.settings;
|
||||
}
|
||||
|
||||
/// Gets the list of supported countries from the MOAT service.
|
||||
Future<List<String>> getCountries() async {
|
||||
_ensureInitialized();
|
||||
return await _api!.countries();
|
||||
}
|
||||
|
||||
/// Gets the country-to-settings map from the MOAT service.
|
||||
Future<Map<String, SettingsResponse>> getMap() async {
|
||||
_ensureInitialized();
|
||||
return await _api!.map();
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,11 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:pluggable_transports_proxy/pluggable_transports_proxy.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/features/tor/data/models/moat.dart';
|
||||
import 'package:weblibre/features/tor/data/services/builtin_bridges.dart';
|
||||
import 'package:weblibre/features/tor/data/services/moat_service.dart';
|
||||
|
||||
part 'builtin_bridges.g.dart';
|
||||
|
||||
|
||||
@@ -18,107 +18,34 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:flutter_tor/flutter_tor.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:pluggable_transports_proxy/pluggable_transports_proxy.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers/lifecycle.dart';
|
||||
import 'package:weblibre/features/tor/data/models/moat.dart';
|
||||
import 'package:weblibre/features/tor/data/services/moat_service.dart';
|
||||
import 'package:weblibre/features/tor/domain/repositories/builtin_bridges.dart';
|
||||
import 'package:weblibre/features/tor/utils/tor_entrypoint.dart';
|
||||
import 'package:weblibre/features/user/data/models/tor_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/tor_settings.dart';
|
||||
|
||||
part 'tor_proxy.g.dart';
|
||||
|
||||
class _TorService {
|
||||
final service = FlutterBackgroundService();
|
||||
|
||||
final _portSubject = BehaviorSubject<int?>.seeded(null);
|
||||
|
||||
ValueStream<int?> get portStream => _portSubject.stream;
|
||||
|
||||
Future<void> startOrReconfigure({
|
||||
ProxyType? proxyType,
|
||||
String? bridgeLines,
|
||||
}) async {
|
||||
await service.startService();
|
||||
service.invoke('startOrReconfigure', {
|
||||
if (proxyType != null) 'proxyType': proxyType.name,
|
||||
if (bridgeLines != null) 'bridgeLines': bridgeLines,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> reconfigure({ProxyType? proxyType, String? bridgeLines}) async {
|
||||
service.invoke('reconfigure', {
|
||||
if (proxyType != null) 'proxyType': proxyType.name,
|
||||
if (bridgeLines != null) 'bridgeLines': bridgeLines,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> requestSync() async {
|
||||
service.invoke("sync");
|
||||
}
|
||||
|
||||
Future<void> sendHeartbeat() async {
|
||||
service.invoke("heartbeat");
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
service.invoke("stop");
|
||||
}
|
||||
|
||||
Future<void> initializeService() async {
|
||||
await service.configure(
|
||||
iosConfiguration: IosConfiguration(autoStart: false),
|
||||
androidConfiguration: AndroidConfiguration(
|
||||
autoStart: false,
|
||||
onStart: onStart,
|
||||
isForegroundMode: true,
|
||||
autoStartOnBoot: false,
|
||||
initialNotificationTitle: 'Tor Service',
|
||||
initialNotificationContent: 'Running in background',
|
||||
),
|
||||
);
|
||||
|
||||
service.on('portUpdate').listen((data) {
|
||||
final port = data!['port'] as int;
|
||||
_portSubject.add((port > -1) ? port : null);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await stop();
|
||||
await _portSubject.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class TorProxyService extends _$TorProxyService {
|
||||
final _tor = _TorService();
|
||||
final _tor = FlutterTor();
|
||||
late StreamController<TorStatus> _statusSyncController;
|
||||
|
||||
Timer? _heartbeatUpdate;
|
||||
//This is managed by widget state changes to resume a timer
|
||||
bool _timerPaused = false;
|
||||
Future<TorStatus> startOrReconfigure({
|
||||
required bool reconfigureIfRunning,
|
||||
}) async {
|
||||
final currentStatus = await _tor.getStatus();
|
||||
|
||||
void _enableHeartbeatTimer() {
|
||||
_heartbeatUpdate?.cancel();
|
||||
_timerPaused = false;
|
||||
_heartbeatUpdate = Timer.periodic(const Duration(seconds: 30), (
|
||||
timer,
|
||||
) async {
|
||||
await _tor.sendHeartbeat();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> startOrReconfigure({bool forceReconnect = false}) async {
|
||||
final currentPort = await requestSync();
|
||||
|
||||
if (currentPort == null || forceReconnect) {
|
||||
if (!currentStatus.isRunning ||
|
||||
currentStatus.socksPort == null ||
|
||||
reconfigureIfRunning) {
|
||||
state = const AsyncLoading();
|
||||
|
||||
final torSettings = await ref
|
||||
@@ -153,10 +80,10 @@ class TorProxyService extends _$TorProxyService {
|
||||
setting = config.mapNotNull(
|
||||
(config) =>
|
||||
config.firstWhereOrNull(
|
||||
(setting) => setting.bridge.type == TransportType.obfs4,
|
||||
(setting) => setting.bridge.type == MoatTransportType.obfs4,
|
||||
) ??
|
||||
config.firstWhereOrNull(
|
||||
(setting) => setting.bridge.type == TransportType.snowflake,
|
||||
(setting) => setting.bridge.type == MoatTransportType.snowflake,
|
||||
),
|
||||
);
|
||||
} else if (torSettings.config != TorConnectionConfig.direct) {
|
||||
@@ -186,8 +113,9 @@ class TorProxyService extends _$TorProxyService {
|
||||
switch (torSettings.config) {
|
||||
TorConnectionConfig.auto => throw UnimplementedError(),
|
||||
TorConnectionConfig.direct => throw UnimplementedError(),
|
||||
TorConnectionConfig.obfs4 => TransportType.obfs4,
|
||||
TorConnectionConfig.snowflake => TransportType.snowflake,
|
||||
TorConnectionConfig.obfs4 => MoatTransportType.obfs4,
|
||||
TorConnectionConfig.snowflake =>
|
||||
MoatTransportType.snowflake,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -198,96 +126,44 @@ class TorProxyService extends _$TorProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
if (setting != null) {
|
||||
switch (setting.bridge.type) {
|
||||
case TransportType.obfs4:
|
||||
await _tor.startOrReconfigure(
|
||||
proxyType: ProxyType.obfs4,
|
||||
bridgeLines: setting.bridge.bridges?.join('\n'),
|
||||
);
|
||||
case TransportType.snowflake:
|
||||
await _tor.startOrReconfigure(
|
||||
proxyType: ProxyType.snowflake,
|
||||
bridgeLines: setting.bridge.bridges?.join('\n'),
|
||||
);
|
||||
case TransportType.meek:
|
||||
case TransportType.meekAzure:
|
||||
case TransportType.webtunnel:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
} else {
|
||||
await _tor.startOrReconfigure();
|
||||
}
|
||||
final config = TorConfiguration(
|
||||
transport: switch (setting?.bridge.type) {
|
||||
MoatTransportType.obfs4 => TransportType.obfs4,
|
||||
MoatTransportType.snowflake => TransportType.snowflake,
|
||||
MoatTransportType.meek => TransportType.meek,
|
||||
MoatTransportType.meekAzure => TransportType.meekAzure,
|
||||
MoatTransportType.webtunnel => TransportType.webtunnel,
|
||||
null => TransportType.none,
|
||||
},
|
||||
bridgeLines: setting?.bridge.bridges ?? [],
|
||||
);
|
||||
|
||||
_enableHeartbeatTimer();
|
||||
await _tor.start(config);
|
||||
return _tor.getStatus();
|
||||
}
|
||||
|
||||
return currentStatus;
|
||||
}
|
||||
|
||||
Future<int?> requestSync() async {
|
||||
final nextPortUpdate = _tor.portStream.first;
|
||||
|
||||
await _tor.requestSync();
|
||||
return nextPortUpdate;
|
||||
Future<TorStatus> requestSync() async {
|
||||
final status = await _tor.getStatus();
|
||||
_statusSyncController.add(status);
|
||||
return status;
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
state = const AsyncLoading();
|
||||
|
||||
_heartbeatUpdate?.cancel();
|
||||
_heartbeatUpdate = null;
|
||||
_timerPaused = false;
|
||||
|
||||
await _tor.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int?> build() async {
|
||||
final portSub = _tor.portStream.distinct().listen((port) {
|
||||
state = AsyncData(port);
|
||||
});
|
||||
|
||||
await _tor.initializeService();
|
||||
|
||||
if (!ref.mounted) return null;
|
||||
|
||||
ref.listen(
|
||||
fireImmediately: true,
|
||||
browserViewLifecycleProvider,
|
||||
(previous, next) {
|
||||
switch (next) {
|
||||
case AppLifecycleState.resumed:
|
||||
if (_timerPaused) {
|
||||
_enableHeartbeatTimer();
|
||||
}
|
||||
case AppLifecycleState.detached:
|
||||
case AppLifecycleState.inactive:
|
||||
case AppLifecycleState.hidden:
|
||||
case AppLifecycleState.paused:
|
||||
case null:
|
||||
if (_heartbeatUpdate?.isActive == true) {
|
||||
_heartbeatUpdate?.cancel();
|
||||
_timerPaused = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error, stackTrace) {
|
||||
logger.e(
|
||||
'Error listening to browserViewLifecycleProvider',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
},
|
||||
);
|
||||
Stream<TorStatus> build() {
|
||||
_statusSyncController = StreamController();
|
||||
|
||||
ref.onDispose(() async {
|
||||
_heartbeatUpdate?.cancel();
|
||||
_heartbeatUpdate = null;
|
||||
_timerPaused = false;
|
||||
|
||||
await portSub.cancel();
|
||||
await _tor.dispose();
|
||||
await _statusSyncController.close();
|
||||
await _tor.stop();
|
||||
});
|
||||
|
||||
return _tor.portStream.valueOrNull;
|
||||
return MergeStream([_tor.statusStream, _statusSyncController.stream]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ part of 'tor_proxy.dart';
|
||||
final torProxyServiceProvider = TorProxyServiceProvider._();
|
||||
|
||||
final class TorProxyServiceProvider
|
||||
extends $AsyncNotifierProvider<TorProxyService, int?> {
|
||||
extends $StreamNotifierProvider<TorProxyService, TorStatus> {
|
||||
TorProxyServiceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
@@ -33,19 +33,19 @@ final class TorProxyServiceProvider
|
||||
TorProxyService create() => TorProxyService();
|
||||
}
|
||||
|
||||
String _$torProxyServiceHash() => r'c63adf96d7b0a865917b8d902071a0b3ae4dfcc6';
|
||||
String _$torProxyServiceHash() => r'be69327bdef8feaa2a43bd83f20168988dcdd228';
|
||||
|
||||
abstract class _$TorProxyService extends $AsyncNotifier<int?> {
|
||||
FutureOr<int?> build();
|
||||
abstract class _$TorProxyService extends $StreamNotifier<TorStatus> {
|
||||
Stream<TorStatus> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<AsyncValue<int?>, int?>;
|
||||
final ref = this.ref as $Ref<AsyncValue<TorStatus>, TorStatus>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<int?>, int?>,
|
||||
AsyncValue<int?>,
|
||||
AnyNotifier<AsyncValue<TorStatus>, TorStatus>,
|
||||
AsyncValue<TorStatus>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
|
||||
@@ -31,11 +31,11 @@ class StartProxyController extends _$StartProxyController {
|
||||
// ignore: document_ignores is used for dialog
|
||||
// ignore: avoid_build_context_in_providers
|
||||
Future<void> maybeStartProxy(BuildContext context) async {
|
||||
final torProxyRunning = await ref
|
||||
final currentStatus = await ref
|
||||
.read(torProxyServiceProvider.notifier)
|
||||
.requestSync();
|
||||
|
||||
if (torProxyRunning == null) {
|
||||
if (!currentStatus.isRunning) {
|
||||
if (context.mounted) {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -47,7 +47,7 @@ class StartProxyController extends _$StartProxyController {
|
||||
if (result == true) {
|
||||
final connection = ref
|
||||
.read(torProxyServiceProvider.notifier)
|
||||
.startOrReconfigure();
|
||||
.startOrReconfigure(reconfigureIfRunning: false);
|
||||
|
||||
ref
|
||||
.read(overlayControllerProvider.notifier)
|
||||
@@ -59,9 +59,6 @@ class StartProxyController extends _$StartProxyController {
|
||||
await connection;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//Reconfigure
|
||||
await ref.read(torProxyServiceProvider.notifier).startOrReconfigure();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ final class StartProxyControllerProvider
|
||||
}
|
||||
|
||||
String _$startProxyControllerHash() =>
|
||||
r'86a015a0c257ea18ad23aa28bdd43d289b6bfc64';
|
||||
r'13c6ad9611a2f9525689d370a0511274aa9c0f03';
|
||||
|
||||
abstract class _$StartProxyController extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/core/design/app_colors.dart';
|
||||
@@ -34,7 +35,32 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final torProxyPort = ref.watch(torProxyServiceProvider);
|
||||
final bootstrapProgress = ref.watch(
|
||||
torProxyServiceProvider.select(
|
||||
(value) => value.value?.bootstrapProgress ?? 0,
|
||||
),
|
||||
);
|
||||
|
||||
final torPendingRequest = useState<bool?>(null);
|
||||
ref.listen(torProxyServiceProvider, (previous, next) {
|
||||
if (next.hasValue && torPendingRequest.value != null) {
|
||||
if (next.requireValue.isRunning != previous?.value?.isRunning ||
|
||||
next.requireValue.bootstrapProgress !=
|
||||
previous?.value?.bootstrapProgress) {
|
||||
torPendingRequest.value = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
final torIsRunning = ref.watch(
|
||||
torProxyServiceProvider.select(
|
||||
(value) => value.value?.isRunning ?? false,
|
||||
),
|
||||
);
|
||||
|
||||
final torIsBusy =
|
||||
torPendingRequest.value != null ||
|
||||
bootstrapProgress > 0 && bootstrapProgress < 100;
|
||||
final torSettings = ref.watch(torSettingsWithDefaultsProvider);
|
||||
|
||||
useOnInitialization(() async {
|
||||
@@ -43,9 +69,10 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
|
||||
ref.listen(torSettingsRepositoryProvider, (previous, next) async {
|
||||
final torService = ref.read(torProxyServiceProvider.notifier);
|
||||
final currentStatus = await torService.requestSync();
|
||||
|
||||
if (await torService.requestSync() != null) {
|
||||
await torService.startOrReconfigure();
|
||||
if (currentStatus.isRunning) {
|
||||
await torService.startOrReconfigure(reconfigureIfRunning: true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -110,17 +137,23 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
}
|
||||
return null; // Use the default color.
|
||||
}),
|
||||
value: torProxyPort.value != null,
|
||||
value: torPendingRequest.value ?? torIsRunning,
|
||||
title: const Text('Tor™ Proxy'),
|
||||
secondary: const Icon(MdiIcons.power),
|
||||
onChanged: torProxyPort.isLoading
|
||||
onChanged: torIsBusy
|
||||
? null
|
||||
: (value) async {
|
||||
if (value) {
|
||||
torPendingRequest.value = true;
|
||||
|
||||
await ref
|
||||
.read(torProxyServiceProvider.notifier)
|
||||
.startOrReconfigure();
|
||||
.startOrReconfigure(
|
||||
reconfigureIfRunning: false,
|
||||
);
|
||||
} else {
|
||||
torPendingRequest.value = false;
|
||||
|
||||
await ref
|
||||
.read(torProxyServiceProvider.notifier)
|
||||
.disconnect();
|
||||
@@ -259,7 +292,7 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
secondary: const Icon(
|
||||
MdiIcons.arrowDecisionAuto,
|
||||
),
|
||||
onChanged: torProxyPort.isLoading
|
||||
onChanged: torIsBusy
|
||||
? null
|
||||
: (value) async {
|
||||
await ref
|
||||
@@ -291,7 +324,7 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
left: 56,
|
||||
right: 24,
|
||||
),
|
||||
onChanged: torProxyPort.isLoading
|
||||
onChanged: torIsBusy
|
||||
? null
|
||||
: (value) async {
|
||||
await ref
|
||||
@@ -330,7 +363,7 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
RadioListTile.adaptive(
|
||||
value: TorConnectionConfig.direct,
|
||||
enabled: !torProxyPort.isLoading,
|
||||
enabled: !torIsBusy,
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 56,
|
||||
right: 24,
|
||||
@@ -342,7 +375,7 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
),
|
||||
RadioListTile.adaptive(
|
||||
value: TorConnectionConfig.obfs4,
|
||||
enabled: !torProxyPort.isLoading,
|
||||
enabled: !torIsBusy,
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 56,
|
||||
right: 24,
|
||||
@@ -354,7 +387,7 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
),
|
||||
RadioListTile.adaptive(
|
||||
value: TorConnectionConfig.snowflake,
|
||||
enabled: !torProxyPort.isLoading,
|
||||
enabled: !torIsBusy,
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 56,
|
||||
right: 24,
|
||||
@@ -378,7 +411,7 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
left: 56,
|
||||
right: 24,
|
||||
),
|
||||
onChanged: torProxyPort.isLoading
|
||||
onChanged: torIsBusy
|
||||
? null
|
||||
: (value) async {
|
||||
if (value != null) {
|
||||
@@ -413,23 +446,24 @@ class TorProxyScreen extends HookConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (torProxyPort.isLoading)
|
||||
const Column(
|
||||
if (torIsBusy)
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 8),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
backgroundColor: AppColors.torBackgroundGrey,
|
||||
color: AppColors.torActiveGreen,
|
||||
value: bootstrapProgress / 100,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Establishing connection...',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (torProxyPort.value != null)
|
||||
)
|
||||
else if (torIsRunning)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0),
|
||||
child: Row(
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:pluggable_transports_proxy/pluggable_transports_proxy.dart';
|
||||
import 'package:tor/tor.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> onStart(ServiceInstance service) async {
|
||||
Timer? timeout;
|
||||
final startedProxyType = <ProxyType>{};
|
||||
|
||||
Tor.init();
|
||||
|
||||
final portSub = Tor.instance.events.stream.listen((port) {
|
||||
service.invoke('portUpdate', {'port': port});
|
||||
});
|
||||
|
||||
Future<void> stopProxies() async {
|
||||
for (final proxyType in startedProxyType) {
|
||||
await IPtProxyController().stop(proxyType);
|
||||
}
|
||||
startedProxyType.clear();
|
||||
}
|
||||
|
||||
Future<void> stopService() async {
|
||||
timeout?.cancel();
|
||||
timeout = null;
|
||||
|
||||
Tor.instance.stop();
|
||||
await stopProxies();
|
||||
|
||||
service.invoke('portUpdate', {'port': -1});
|
||||
|
||||
await portSub.cancel();
|
||||
|
||||
await service.stopSelf();
|
||||
}
|
||||
|
||||
void addHeartbeat() {
|
||||
timeout?.cancel();
|
||||
//Kill service after 1 hour of inactivity
|
||||
timeout = Timer(const Duration(hours: 1), () async {
|
||||
// logger.i('Terminating tor due to timeout');
|
||||
await stopService();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> startService(ProxyType? proxyType, String? bridgeLines) async {
|
||||
if (proxyType != null) {
|
||||
final port = await IPtProxyController().start(proxyType, "");
|
||||
startedProxyType.add(proxyType);
|
||||
|
||||
switch (proxyType) {
|
||||
case ProxyType.obfs4:
|
||||
await Tor.instance.start(obfs4Port: port, bridgeLines: bridgeLines);
|
||||
case ProxyType.meekLite:
|
||||
throw UnimplementedError();
|
||||
case ProxyType.webtunnel:
|
||||
throw UnimplementedError();
|
||||
case ProxyType.snowflake:
|
||||
await Tor.instance.start(
|
||||
snowflakePort: port,
|
||||
bridgeLines: bridgeLines,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await Tor.instance.start();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reconfigureService(
|
||||
ProxyType? proxyType,
|
||||
String? bridgeLines,
|
||||
) async {
|
||||
if (Tor.instance.hasClient) {
|
||||
await stopProxies();
|
||||
|
||||
if (proxyType != null) {
|
||||
final port = await IPtProxyController().start(proxyType, "");
|
||||
startedProxyType.add(proxyType);
|
||||
|
||||
switch (proxyType) {
|
||||
case ProxyType.obfs4:
|
||||
await Tor.instance.reconfigure(
|
||||
obfs4Port: port,
|
||||
bridgeLines: bridgeLines,
|
||||
);
|
||||
case ProxyType.meekLite:
|
||||
throw UnimplementedError();
|
||||
case ProxyType.webtunnel:
|
||||
throw UnimplementedError();
|
||||
case ProxyType.snowflake:
|
||||
await Tor.instance.reconfigure(
|
||||
snowflakePort: port,
|
||||
bridgeLines: bridgeLines,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await Tor.instance.reconfigure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.on("startOrReconfigure").listen((event) async {
|
||||
final proxyType = ProxyType.values.firstWhereOrNull(
|
||||
(x) => x.name == event?['proxyType'],
|
||||
);
|
||||
|
||||
await reconfigureService(proxyType, event?['bridgeLines'] as String?);
|
||||
|
||||
if (!Tor.instance.hasClient) {
|
||||
await startService(proxyType, event?['bridgeLines'] as String?);
|
||||
}
|
||||
});
|
||||
|
||||
service.on("reconfigure").listen((event) async {
|
||||
final proxyType = ProxyType.values.firstWhereOrNull(
|
||||
(x) => x.name == event?['proxyType'],
|
||||
);
|
||||
|
||||
await reconfigureService(proxyType, event?['bridgeLines'] as String?);
|
||||
});
|
||||
|
||||
service.on("heartbeat").listen((event) {
|
||||
// logger.d('Received tor heartbeat');
|
||||
addHeartbeat();
|
||||
});
|
||||
|
||||
service.on("sync").listen((event) {
|
||||
service.invoke('portUpdate', {'port': Tor.instance.port});
|
||||
});
|
||||
|
||||
service.on("stop").listen((event) async {
|
||||
await stopService();
|
||||
});
|
||||
}
|
||||
@@ -93,7 +93,9 @@ Future<WebPageInfo> pageInfo(
|
||||
(tabState.isPrivate == false &&
|
||||
torSettings.proxyRegularTabsMode == TorRegularTabProxyMode.all) ||
|
||||
(tabState.isPrivate == true && torSettings.proxyPrivateTabsTor)) {
|
||||
proxyPort = await ref.read(torProxyServiceProvider.future);
|
||||
proxyPort = await ref.read(
|
||||
torProxyServiceProvider.selectAsync((value) => value.socksPort),
|
||||
);
|
||||
|
||||
if (proxyPort == null) {
|
||||
throw Exception('Could not proxy request');
|
||||
|
||||
@@ -162,7 +162,7 @@ final class PageInfoProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$pageInfoHash() => r'89b1cd86bb7ed7cfe507650cca33d6b6b72ca770';
|
||||
String _$pageInfoHash() => r'19116a520df214240254f95989818c1ac3bde6f6';
|
||||
|
||||
final class PageInfoFamily extends $Family
|
||||
with
|
||||
|
||||
+2
-5
@@ -26,7 +26,6 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_auto_size_text: ^4.1.0
|
||||
flutter_background_service: ^5.1.0
|
||||
flutter_hooks: ^0.21.3+1
|
||||
flutter_markdown: ^0.7.7+1
|
||||
flutter_material_design_icons: ^1.1.7447
|
||||
@@ -35,6 +34,8 @@ dependencies:
|
||||
flutter_reorderable_grid_view: ^5.5.2
|
||||
flutter_slidable: ^4.0.3
|
||||
flutter_svg: ^2.2.3
|
||||
flutter_tor:
|
||||
path: ../packages/flutter_tor
|
||||
go_router: ^17.0.1
|
||||
google_fonts: ^6.3.3
|
||||
graphview: ^1.5.1
|
||||
@@ -58,8 +59,6 @@ dependencies:
|
||||
path: ^1.9.1
|
||||
path_provider: ^2.1.5
|
||||
permission_handler: ^12.0.1
|
||||
pluggable_transports_proxy:
|
||||
path: ../packages/pluggable_transports_proxy
|
||||
pretty_qr_code: ^3.5.0
|
||||
quick_actions: ^1.1.0
|
||||
riverpod: ^3.1.0
|
||||
@@ -85,8 +84,6 @@ dependencies:
|
||||
synchronized: ^3.4.0
|
||||
text_scroll: ^0.2.1
|
||||
timeago: ^3.7.1
|
||||
tor:
|
||||
path: ../packages/tor
|
||||
uri_to_file:
|
||||
git:
|
||||
url: https://github.com/FaFre/uri-to-file.git
|
||||
|
||||
Reference in New Issue
Block a user