Add proxy routing and sing-box support

This commit is contained in:
Fabian Freund
2026-05-22 18:16:31 +02:00
parent 51289f1266
commit a5974617aa
262 changed files with 32003 additions and 3962 deletions
@@ -0,0 +1,174 @@
import 'dart:async';
import 'dart:ui';
import 'package:drift/native.dart';
import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_tor/flutter_tor.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/data/database/functions/lexo_rank_functions.dart';
import 'package:weblibre/data/database/functions/url_functions.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:weblibre/features/proxy/data/proxy_connection.dart';
import 'package:weblibre/features/proxy/domain/repositories/container_proxy.dart';
import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart';
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart';
import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test(
'container assignments ensure sing-box profiles are running before syncing',
() async {
final profileId = SingboxProxyConnectionId('profile-1');
final assignedContainer = _container(
id: 'container-1',
contextId: 'context-a',
proxyConnectionId: profileId,
);
final db = TabDatabase(
NativeDatabase.memory(
setup: (database) {
registerLexorankFunctions(database);
registerUrlFunctions(database);
},
),
);
final containerProxyRepository = _FakeContainerProxyRepository();
final containerRepository = _FakeContainerRepository([assignedContainer]);
final runtimeRepository = _FakeSingboxProxyRuntimeRepository();
final container = ProviderContainer(
overrides: [
tabDatabaseProvider.overrideWith((ref) => db),
containerProxyRepositoryProvider.overrideWith(
() => containerProxyRepository,
),
containerRepositoryProvider.overrideWith(() => containerRepository),
singboxProxyRuntimeRepositoryProvider.overrideWith(
() => runtimeRepository,
),
torProxyServiceProvider.overrideWith(_FakeTorProxyService.new),
proxyRoutingSettingsWithDefaultsProvider.overrideWith(
(ref) => ProxyRoutingSettings.withDefaults(),
),
watchContainersWithCountProvider.overrideWith(
(ref) => Stream.value([assignedContainer]),
),
watchAllAssignedSitesProvider.overrideWith(
(ref) => Stream.value(const <SiteAssignment>[]),
),
watchIsolatedContextContainerMapProvider.overrideWith(
(ref) => Stream.value(const <String, Set<String>>{}),
),
],
);
addTearDown(() async {
container.dispose();
await db.close();
});
final subscription = container.listen<void>(
proxySettingsReplicationProvider,
(previous, next) {},
fireImmediately: true,
);
addTearDown(subscription.close);
await pumpEventQueue();
expect(runtimeRepository.ensuredProxyConnectionIds, [profileId]);
expect(containerProxyRepository.setContainerProxyCalls, [
('context-a', profileId.encode()),
]);
},
);
}
ContainerDataWithCount _container({
required String id,
required String contextId,
required ProxyConnectionId proxyConnectionId,
}) {
return ContainerDataWithCount(
id: id,
name: 'Container',
color: const Color(0xFF336699),
orderKey: 'a',
metadata: ContainerMetadata.withDefaults(
contextualIdentity: contextId,
proxyConnectionId: proxyConnectionId,
),
tabCount: 0,
);
}
class _FakeContainerProxyRepository extends ContainerProxyRepository {
final setContainerProxyCalls = <(String, String)>[];
@override
Future<void> setTorProxyPort(int? port) async {}
@override
Future<void> setSiteAssignments(List<SiteAssignment> assignements) async {}
@override
Future<void> setContainerProxy(String contextId, String proxyId) async {
setContainerProxyCalls.add((contextId, proxyId));
}
@override
Future<void> clearContainerProxy(String contextId) async {}
@override
void build() {}
}
class _FakeContainerRepository extends ContainerRepository {
final List<ContainerDataWithCount> containers;
_FakeContainerRepository(this.containers);
@override
Future<List<ContainerDataWithCount>> getAllContainersWithCount() async {
return containers;
}
@override
void build() {}
}
class _FakeSingboxProxyRuntimeRepository extends SingboxProxyRuntimeRepository {
final ensuredProxyConnectionIds = <SingboxProxyConnectionId>[];
@override
Future<void> ensureProxyConnectionAvailable(
SingboxProxyConnectionId connectionId,
) async {
ensuredProxyConnectionIds.add(connectionId);
}
@override
Future<SingboxProxyRuntimeState> build() async {
return SingboxProxyRuntimeState(
status: SingboxProxyRuntimeStatus.stopped,
endpoints: [],
);
}
}
class _FakeTorProxyService extends TorProxyService {
@override
Stream<TorStatus> build() async* {
// Mirror production: the real TorProxyService seeds the provider with the
// current status so downstream `selectAsync` paths don't stay in
// AsyncLoading forever. Tor is stopped in these tests.
yield TorStatus(isRunning: false, bootstrapProgress: 0);
}
}
@@ -0,0 +1,33 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/proxy/data/proxy_connection.dart';
void main() {
group('ContainerMetadata proxy serialization', () {
test('round-trips proxyConnectionId through JSON', () {
final metadata = ContainerMetadata.withDefaults(
proxyConnectionId: const SingboxProxyConnectionId('profile-1'),
clearDataOnExit: true,
);
final json = metadata.toJson();
final restored = ContainerMetadata.fromJson(json);
expect(
json['proxyConnectionId'],
const SingboxProxyConnectionId('profile-1').encode(),
);
expect(json['clearDataOnExit'], isTrue);
expect(restored.proxyConnectionId, metadata.proxyConnectionId);
expect(restored.usesTorProxy, isFalse);
});
test('usesTorProxy is true only for the Tor connection id', () {
final tor = ContainerMetadata.withDefaults(
proxyConnectionId: const TorProxyConnectionId(),
);
expect(tor.usesTorProxy, isTrue);
});
});
}
@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_edit.dart';
import 'package:weblibre/features/proxy/data/proxy_connection.dart';
import 'package:weblibre/features/proxy/domain/providers/proxy_connection_options.dart';
void main() {
testWidgets(
'clearing proxy selection does not leave cookie isolation enabled in create mode',
(tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
proxyConnectionOptionsProvider.overrideWith(
(ref) => const <ProxyConnectionOption>[],
),
],
child: MaterialApp(
home: ContainerEditScreen.create(
initialContainer: ContainerData(
id: 'container-1',
color: Colors.blue,
orderKey: 'a',
metadata: ContainerMetadata.withDefaults(
proxyConnectionId: SingboxProxyConnectionId('missing-proxy'),
),
),
),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Proxy Connection'));
await tester.pumpAndSettle();
await tester.tap(find.text('Clear'));
await tester.pumpAndSettle();
final cookieIsolationTile = tester.widget<SwitchListTile>(
find.widgetWithText(SwitchListTile, 'Cookie Isolation'),
);
expect(cookieIsolationTile.value, isFalse);
expect(find.text('None'), findsOneWidget);
},
);
testWidgets(
'dismissing proxy picker does not leave cookie isolation enabled in create mode',
(tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
proxyConnectionOptionsProvider.overrideWith(
(ref) => const <ProxyConnectionOption>[],
),
],
child: MaterialApp(
home: ContainerEditScreen.create(
initialContainer: ContainerData(
id: 'container-1',
color: Colors.blue,
orderKey: 'a',
),
),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Proxy Connection'));
await tester.pumpAndSettle();
await tester.tapAt(const Offset(8, 8));
await tester.pumpAndSettle();
final cookieIsolationTile = tester.widget<SwitchListTile>(
find.widgetWithText(SwitchListTile, 'Cookie Isolation'),
);
expect(cookieIsolationTile.value, isFalse);
expect(find.text('New Container'), findsOneWidget);
},
);
}