prepare for multiple apps
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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 '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:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/tor/data/models/moat.dart';
|
||||
|
||||
part 'builtin_bridges.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class BuiltinBridgesService extends _$BuiltinBridgesService {
|
||||
late final _bridgeFileFuture = path_provider
|
||||
.getApplicationSupportDirectory()
|
||||
.then((dir) => File(p.join(dir.path, 'builtin-bridges.json')));
|
||||
|
||||
Future<DateTime?> lastUpdate() async {
|
||||
final bridgeFile = await _bridgeFileFuture;
|
||||
if (!await bridgeFile.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bridgeFile.lastModified();
|
||||
}
|
||||
|
||||
Future<void> updateStoredBuiltinBridges(BuiltInBridges bridges) async {
|
||||
final bridgeFile = await _bridgeFileFuture;
|
||||
await bridgeFile.writeAsString(jsonEncode(bridges.toJson()), flush: true);
|
||||
}
|
||||
|
||||
Future<BuiltInBridges?> getStoredBuiltinBridges() async {
|
||||
final bridgeFile = await _bridgeFileFuture;
|
||||
if (!await bridgeFile.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final content = await bridgeFile.readAsString();
|
||||
return BuiltInBridges.fromJson(jsonDecode(content) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<BuiltInBridges> getBundledBuiltinBridges() async {
|
||||
final content = await rootBundle.loadString(
|
||||
'assets/preferences/builtin-bridges.json',
|
||||
);
|
||||
return BuiltInBridges.fromJson(jsonDecode(content) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
void build() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'builtin_bridges.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(BuiltinBridgesService)
|
||||
final builtinBridgesServiceProvider = BuiltinBridgesServiceProvider._();
|
||||
|
||||
final class BuiltinBridgesServiceProvider
|
||||
extends $NotifierProvider<BuiltinBridgesService, void> {
|
||||
BuiltinBridgesServiceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'builtinBridgesServiceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$builtinBridgesServiceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
BuiltinBridgesService create() => BuiltinBridgesService();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$builtinBridgesServiceHash() =>
|
||||
r'beacb3c9d8c5a7179c7c9cad8024d128b09241f5';
|
||||
|
||||
abstract class _$BuiltinBridgesService extends $Notifier<void> {
|
||||
void build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<void, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<void, void>,
|
||||
void,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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-2026 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user