prepare for multiple apps

This commit is contained in:
Fabian Freund
2026-04-06 12:23:11 +02:00
parent bd1600e8dc
commit 5afc323f04
904 changed files with 29 additions and 29 deletions
@@ -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();
}
}