setting to exclude proxy from global routing
This commit is contained in:
@@ -396,6 +396,19 @@ Future<bool> isTabTunneled(Ref ref, String? tabId) async {
|
||||
|
||||
return containerData?.metadata.proxyConnectionId != null;
|
||||
case ProxyRegularTabRoutingMode.all:
|
||||
final containerData = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getTabContainerData(tabState.id);
|
||||
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
if (containerData?.metadata.proxyConnectionId != null) {
|
||||
return true;
|
||||
}
|
||||
if (containerData?.metadata.bypassGlobalProxy == true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return proxyRoutingSettings.regularTabsProxyConnectionId != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +258,7 @@ final class IsTabTunneledProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$isTabTunneledHash() => r'cb3a3afe5b94f7e9c77d6a32266afbb8a225c259';
|
||||
String _$isTabTunneledHash() => r'd36607ff812f71870af32ce59c2dbe898a9c1917';
|
||||
|
||||
final class IsTabTunneledFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<bool>, String?> {
|
||||
|
||||
+116
-27
@@ -33,10 +33,45 @@ import 'package:weblibre/features/user/domain/repositories/proxy_routing_setting
|
||||
|
||||
part 'proxy_settings_replication.g.dart';
|
||||
|
||||
sealed class _ProxyAssignment with FastEquatable {
|
||||
_ProxyAssignment();
|
||||
|
||||
factory _ProxyAssignment.inherit() = _InheritProxyAssignment;
|
||||
|
||||
factory _ProxyAssignment.direct(String scopeId) = _DirectProxyAssignment;
|
||||
|
||||
factory _ProxyAssignment.explicit(String proxyId) = _ExplicitProxyAssignment;
|
||||
}
|
||||
|
||||
final class _InheritProxyAssignment extends _ProxyAssignment {
|
||||
_InheritProxyAssignment();
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => const ['inherit'];
|
||||
}
|
||||
|
||||
final class _DirectProxyAssignment extends _ProxyAssignment {
|
||||
final String scopeId;
|
||||
|
||||
_DirectProxyAssignment(this.scopeId);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [scopeId];
|
||||
}
|
||||
|
||||
final class _ExplicitProxyAssignment extends _ProxyAssignment {
|
||||
final String proxyId;
|
||||
|
||||
_ExplicitProxyAssignment(this.proxyId);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [proxyId];
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
var _isolatedProxyAssignments = <String, String>{};
|
||||
var _appliedContainerProxies = <String, String?>{};
|
||||
var _isolatedProxyAssignments = <String, _ProxyAssignment>{};
|
||||
var _appliedContainerProxies = <String, _ProxyAssignment>{};
|
||||
|
||||
final _recomputeLock = Lock();
|
||||
var _recomputeDirty = false;
|
||||
@@ -82,33 +117,71 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getAllContainersWithCount();
|
||||
|
||||
final containerProxyIds = <String, String>{
|
||||
final containerAssignments = <String, _ProxyAssignment>{
|
||||
for (final c in containers)
|
||||
if (c.metadata.proxyConnectionId case final proxyId?)
|
||||
c.id: proxyId.encode(),
|
||||
if (c.metadata.contextualIdentity case final contextId?)
|
||||
c.id: switch (c.metadata.proxyConnectionId) {
|
||||
final proxyId? => _ProxyAssignment.explicit(proxyId.encode()),
|
||||
null when c.metadata.bypassGlobalProxy => _ProxyAssignment.direct(
|
||||
contextId,
|
||||
),
|
||||
null => _ProxyAssignment.inherit(),
|
||||
},
|
||||
};
|
||||
|
||||
final newAssignments = <String, String>{};
|
||||
final newAssignments = <String, _ProxyAssignment>{};
|
||||
for (final entry in contextContainerMap.entries) {
|
||||
final assignments = entry.value
|
||||
.map((containerId) => containerAssignments[containerId])
|
||||
.nonNulls
|
||||
.toList();
|
||||
|
||||
if (assignments.isEmpty) continue;
|
||||
|
||||
final proxyIds =
|
||||
entry.value
|
||||
.map((containerId) => containerProxyIds[containerId])
|
||||
.nonNulls
|
||||
assignments
|
||||
.whereType<_ExplicitProxyAssignment>()
|
||||
.map((assignment) => assignment.proxyId)
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort();
|
||||
final directScopeIds =
|
||||
assignments
|
||||
.whereType<_DirectProxyAssignment>()
|
||||
.map((assignment) => assignment.scopeId)
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort();
|
||||
final hasInheritedAssignment = assignments.any(
|
||||
(assignment) => assignment is _InheritProxyAssignment,
|
||||
);
|
||||
|
||||
if (proxyIds.isEmpty) continue;
|
||||
final chosenAssignment = proxyIds.isNotEmpty
|
||||
? _ProxyAssignment.explicit(proxyIds.first)
|
||||
: directScopeIds.isNotEmpty && !hasInheritedAssignment
|
||||
? _ProxyAssignment.direct(directScopeIds.first)
|
||||
: _ProxyAssignment.inherit();
|
||||
if (chosenAssignment is! _InheritProxyAssignment) {
|
||||
newAssignments[entry.key] = chosenAssignment;
|
||||
}
|
||||
final chosenLabel = switch (chosenAssignment) {
|
||||
_DirectProxyAssignment(:final scopeId) => 'direct:$scopeId',
|
||||
_ExplicitProxyAssignment(:final proxyId) => proxyId,
|
||||
_InheritProxyAssignment() => 'inherit',
|
||||
};
|
||||
|
||||
newAssignments[entry.key] = proxyIds.first;
|
||||
|
||||
if (proxyIds.length > 1) {
|
||||
final distinctAssignmentCount =
|
||||
proxyIds.length +
|
||||
directScopeIds.length +
|
||||
(hasInheritedAssignment ? 1 : 0);
|
||||
if (distinctAssignmentCount > 1) {
|
||||
// Isolation contexts can hold multiple containers; if they disagree on
|
||||
// a proxy connection the alias is forced to pick one. Surface this so
|
||||
// the user can split the containers across isolation contexts.
|
||||
// routing, the alias is forced to pick one. Surface this so the user
|
||||
// can split the containers across isolation contexts.
|
||||
logger.w(
|
||||
'Isolation context ${entry.key} has containers with multiple '
|
||||
'proxy connections (${proxyIds.join(', ')}); using ${proxyIds.first}',
|
||||
'proxy routing assignments '
|
||||
'(${[if (hasInheritedAssignment) 'inherit', ...directScopeIds.map((id) => 'direct:$id'), ...proxyIds].join(', ')}); using $chosenLabel',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -126,9 +199,7 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
for (final MapEntry(:key, :value) in newAssignments.entries) {
|
||||
if (_isolatedProxyAssignments[key] == value) continue;
|
||||
|
||||
await ref
|
||||
.read(containerProxyRepositoryProvider.notifier)
|
||||
.setContainerProxy(key, value);
|
||||
await _applyProxyAssignment(key, value);
|
||||
}
|
||||
|
||||
_isolatedProxyAssignments = newAssignments;
|
||||
@@ -260,15 +331,22 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
) async {
|
||||
if (containers == null) return;
|
||||
|
||||
final desired = <String, String?>{};
|
||||
final desired = <String, _ProxyAssignment>{};
|
||||
for (final container in containers) {
|
||||
final contextId = container.metadata.contextualIdentity;
|
||||
if (contextId == null || contextId.isEmpty) continue;
|
||||
desired[contextId] = container.metadata.proxyConnectionId?.encode();
|
||||
final proxyConnectionId = container.metadata.proxyConnectionId;
|
||||
desired[contextId] = proxyConnectionId != null
|
||||
? _ProxyAssignment.explicit(proxyConnectionId.encode())
|
||||
: container.metadata.bypassGlobalProxy
|
||||
? _ProxyAssignment.direct(contextId)
|
||||
: _ProxyAssignment.inherit();
|
||||
}
|
||||
|
||||
final repo = ref.read(containerProxyRepositoryProvider.notifier);
|
||||
final nextApplied = Map<String, String?>.from(_appliedContainerProxies);
|
||||
final nextApplied = Map<String, _ProxyAssignment>.from(
|
||||
_appliedContainerProxies,
|
||||
);
|
||||
|
||||
for (final contextId in _appliedContainerProxies.keys.toSet().difference(
|
||||
desired.keys.toSet(),
|
||||
@@ -291,11 +369,7 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (value != null) {
|
||||
await repo.setContainerProxy(key, value);
|
||||
} else {
|
||||
await repo.clearContainerProxy(key);
|
||||
}
|
||||
await _applyProxyAssignment(key, value);
|
||||
nextApplied[key] = value;
|
||||
} catch (error, stackTrace) {
|
||||
logger.e(
|
||||
@@ -310,4 +384,19 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
|
||||
_appliedContainerProxies = nextApplied;
|
||||
}
|
||||
|
||||
Future<void> _applyProxyAssignment(
|
||||
String contextId,
|
||||
_ProxyAssignment assignment,
|
||||
) async {
|
||||
final repo = ref.read(containerProxyRepositoryProvider.notifier);
|
||||
switch (assignment) {
|
||||
case _ExplicitProxyAssignment(:final proxyId):
|
||||
await repo.setContainerProxy(contextId, proxyId);
|
||||
case _DirectProxyAssignment(:final scopeId):
|
||||
await repo.setContainerDirectConnection(contextId, scopeId: scopeId);
|
||||
case _InheritProxyAssignment():
|
||||
await repo.clearContainerProxy(contextId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ final class ProxySettingsReplicationProvider
|
||||
}
|
||||
|
||||
String _$proxySettingsReplicationHash() =>
|
||||
r'42dcd4ec7bc5a00804f729178b99c56e8c917850';
|
||||
r'cdb624b7f7806a7ce4218da350253ed3d29986d0';
|
||||
|
||||
abstract class _$ProxySettingsReplication extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
+33
-7
@@ -249,9 +249,9 @@ Future<ProxyConnectionId?> _proxyConnectionIdForLoadError(
|
||||
|
||||
if (contextId != null && contextId.isNotEmpty && contextId != 'general') {
|
||||
final isolatedContextProxyConnectionId =
|
||||
await _isolatedContextProxyConnectionId(ref, contextId);
|
||||
await _isolatedContextProxyAssignment(ref, contextId);
|
||||
if (isolatedContextProxyConnectionId != null) {
|
||||
return isolatedContextProxyConnectionId;
|
||||
return isolatedContextProxyConnectionId.proxyConnectionId;
|
||||
}
|
||||
|
||||
final contextContainer = await ref
|
||||
@@ -262,6 +262,9 @@ Future<ProxyConnectionId?> _proxyConnectionIdForLoadError(
|
||||
if (contextProxyConnectionId != null) {
|
||||
return contextProxyConnectionId;
|
||||
}
|
||||
if (contextContainer?.metadata.bypassGlobalProxy == true) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final tabContainer = await ref
|
||||
@@ -271,6 +274,9 @@ Future<ProxyConnectionId?> _proxyConnectionIdForLoadError(
|
||||
if (tabProxyConnectionId != null) {
|
||||
return tabProxyConnectionId;
|
||||
}
|
||||
if (tabContainer?.metadata.bypassGlobalProxy == true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (routing.regularTabsMode == ProxyRegularTabRoutingMode.all) {
|
||||
return routing.regularTabsProxyConnectionId;
|
||||
@@ -279,7 +285,12 @@ Future<ProxyConnectionId?> _proxyConnectionIdForLoadError(
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<ProxyConnectionId?> _isolatedContextProxyConnectionId(
|
||||
typedef _LoadErrorProxyAssignment = ({
|
||||
bool direct,
|
||||
ProxyConnectionId? proxyConnectionId,
|
||||
});
|
||||
|
||||
Future<_LoadErrorProxyAssignment?> _isolatedContextProxyAssignment(
|
||||
WidgetRef ref,
|
||||
String contextId,
|
||||
) async {
|
||||
@@ -296,17 +307,32 @@ Future<ProxyConnectionId?> _isolatedContextProxyConnectionId(
|
||||
final containers = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getAllContainersWithCount();
|
||||
final matchedContainers = containers.where(
|
||||
(container) => containerIds.contains(container.id),
|
||||
);
|
||||
final proxyIds =
|
||||
containers
|
||||
.where((container) => containerIds.contains(container.id))
|
||||
matchedContainers
|
||||
.map((container) => container.metadata.proxyConnectionId?.encode())
|
||||
.nonNulls
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort();
|
||||
|
||||
if (proxyIds.isEmpty) return null;
|
||||
return ProxyConnectionId.decode(proxyIds.first);
|
||||
if (proxyIds.isNotEmpty) {
|
||||
return (
|
||||
direct: false,
|
||||
proxyConnectionId: ProxyConnectionId.decode(proxyIds.first),
|
||||
);
|
||||
}
|
||||
final matchedContainerList = matchedContainers.toList();
|
||||
if (matchedContainerList.isNotEmpty &&
|
||||
matchedContainerList.every(
|
||||
(container) => container.metadata.bypassGlobalProxy,
|
||||
)) {
|
||||
return (direct: true, proxyConnectionId: null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
typedef _PendingProxyLoadError = ({
|
||||
|
||||
@@ -50,6 +50,9 @@ class ContainerMetadata with FastEquatable {
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool excludeFromIndex;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool bypassGlobalProxy;
|
||||
|
||||
final List<Uri>? assignedSites;
|
||||
|
||||
ContainerMetadata({
|
||||
@@ -58,6 +61,7 @@ class ContainerMetadata with FastEquatable {
|
||||
required this.proxyConnectionId,
|
||||
required this.clearDataOnExit,
|
||||
required this.excludeFromIndex,
|
||||
required this.bypassGlobalProxy,
|
||||
required this.assignedSites,
|
||||
});
|
||||
|
||||
@@ -67,6 +71,7 @@ class ContainerMetadata with FastEquatable {
|
||||
ProxyConnectionId? proxyConnectionId,
|
||||
bool? clearDataOnExit,
|
||||
bool? excludeFromIndex,
|
||||
bool? bypassGlobalProxy,
|
||||
List<Uri>? assignedSites,
|
||||
}) : this(
|
||||
iconData: iconData,
|
||||
@@ -74,6 +79,7 @@ class ContainerMetadata with FastEquatable {
|
||||
proxyConnectionId: proxyConnectionId,
|
||||
clearDataOnExit: clearDataOnExit ?? false,
|
||||
excludeFromIndex: excludeFromIndex ?? false,
|
||||
bypassGlobalProxy: bypassGlobalProxy ?? false,
|
||||
assignedSites: assignedSites,
|
||||
);
|
||||
|
||||
@@ -91,6 +97,7 @@ class ContainerMetadata with FastEquatable {
|
||||
proxyConnectionId,
|
||||
clearDataOnExit,
|
||||
excludeFromIndex,
|
||||
bypassGlobalProxy,
|
||||
assignedSites,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ abstract class _$ContainerMetadataCWProxy {
|
||||
|
||||
ContainerMetadata excludeFromIndex(bool excludeFromIndex);
|
||||
|
||||
ContainerMetadata bypassGlobalProxy(bool bypassGlobalProxy);
|
||||
|
||||
ContainerMetadata assignedSites(List<Uri>? assignedSites);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
@@ -32,6 +34,7 @@ abstract class _$ContainerMetadataCWProxy {
|
||||
ProxyConnectionId? proxyConnectionId,
|
||||
bool clearDataOnExit,
|
||||
bool excludeFromIndex,
|
||||
bool bypassGlobalProxy,
|
||||
List<Uri>? assignedSites,
|
||||
});
|
||||
}
|
||||
@@ -62,6 +65,10 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
ContainerMetadata excludeFromIndex(bool excludeFromIndex) =>
|
||||
call(excludeFromIndex: excludeFromIndex);
|
||||
|
||||
@override
|
||||
ContainerMetadata bypassGlobalProxy(bool bypassGlobalProxy) =>
|
||||
call(bypassGlobalProxy: bypassGlobalProxy);
|
||||
|
||||
@override
|
||||
ContainerMetadata assignedSites(List<Uri>? assignedSites) =>
|
||||
call(assignedSites: assignedSites);
|
||||
@@ -80,6 +87,7 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
Object? proxyConnectionId = const $CopyWithPlaceholder(),
|
||||
Object? clearDataOnExit = const $CopyWithPlaceholder(),
|
||||
Object? excludeFromIndex = const $CopyWithPlaceholder(),
|
||||
Object? bypassGlobalProxy = const $CopyWithPlaceholder(),
|
||||
Object? assignedSites = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return ContainerMetadata(
|
||||
@@ -107,6 +115,12 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
? _value.excludeFromIndex
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: excludeFromIndex as bool,
|
||||
bypassGlobalProxy:
|
||||
bypassGlobalProxy == const $CopyWithPlaceholder() ||
|
||||
bypassGlobalProxy == null
|
||||
? _value.bypassGlobalProxy
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: bypassGlobalProxy as bool,
|
||||
assignedSites: assignedSites == const $CopyWithPlaceholder()
|
||||
? _value.assignedSites
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
@@ -247,6 +261,7 @@ ContainerMetadata _$ContainerMetadataFromJson(Map<String, dynamic> json) =>
|
||||
),
|
||||
clearDataOnExit: json['clearDataOnExit'] as bool? ?? false,
|
||||
excludeFromIndex: json['excludeFromIndex'] as bool? ?? false,
|
||||
bypassGlobalProxy: json['bypassGlobalProxy'] as bool? ?? false,
|
||||
assignedSites: (json['assignedSites'] as List<dynamic>?)
|
||||
?.map((e) => Uri.parse(e as String))
|
||||
.toList(),
|
||||
@@ -263,6 +278,7 @@ Map<String, dynamic> _$ContainerMetadataToJson(
|
||||
'proxyConnectionId': _proxyConnectionIdToJson(instance.proxyConnectionId),
|
||||
'clearDataOnExit': instance.clearDataOnExit,
|
||||
'excludeFromIndex': instance.excludeFromIndex,
|
||||
'bypassGlobalProxy': instance.bypassGlobalProxy,
|
||||
'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(),
|
||||
};
|
||||
|
||||
|
||||
@@ -195,8 +195,8 @@ Stream<List<SiteAssignment>> watchAllAssignedSites(Ref ref) {
|
||||
/// aliases for isolated contexts.
|
||||
///
|
||||
/// Returns a map from isolation context ID to the set of container IDs it
|
||||
/// appears in. An isolation context needs a proxy alias if ANY of its
|
||||
/// associated containers has a proxy connection assigned.
|
||||
/// appears in. An isolation context needs an explicit routing alias if any
|
||||
/// associated container has a proxy connection or bypasses global routing.
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<Map<String, Set<String>>> watchIsolatedContextContainerMap(Ref ref) {
|
||||
final db = ref.watch(tabDatabaseProvider);
|
||||
|
||||
@@ -1151,8 +1151,8 @@ String _$watchAllAssignedSitesHash() =>
|
||||
/// aliases for isolated contexts.
|
||||
///
|
||||
/// Returns a map from isolation context ID to the set of container IDs it
|
||||
/// appears in. An isolation context needs a proxy alias if ANY of its
|
||||
/// associated containers has a proxy connection assigned.
|
||||
/// appears in. An isolation context needs an explicit routing alias if any
|
||||
/// associated container has a proxy connection or bypasses global routing.
|
||||
|
||||
@ProviderFor(watchIsolatedContextContainerMap)
|
||||
final watchIsolatedContextContainerMapProvider =
|
||||
@@ -1163,8 +1163,8 @@ final watchIsolatedContextContainerMapProvider =
|
||||
/// aliases for isolated contexts.
|
||||
///
|
||||
/// Returns a map from isolation context ID to the set of container IDs it
|
||||
/// appears in. An isolation context needs a proxy alias if ANY of its
|
||||
/// associated containers has a proxy connection assigned.
|
||||
/// appears in. An isolation context needs an explicit routing alias if any
|
||||
/// associated container has a proxy connection or bypasses global routing.
|
||||
|
||||
final class WatchIsolatedContextContainerMapProvider
|
||||
extends
|
||||
@@ -1181,8 +1181,8 @@ final class WatchIsolatedContextContainerMapProvider
|
||||
/// aliases for isolated contexts.
|
||||
///
|
||||
/// Returns a map from isolation context ID to the set of container IDs it
|
||||
/// appears in. An isolation context needs a proxy alias if ANY of its
|
||||
/// associated containers has a proxy connection assigned.
|
||||
/// appears in. An isolation context needs an explicit routing alias if any
|
||||
/// associated container has a proxy connection or bypasses global routing.
|
||||
WatchIsolatedContextContainerMapProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
|
||||
+28
@@ -99,6 +99,9 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
final excludeFromIndex = useState(
|
||||
initialContainer.metadata.excludeFromIndex,
|
||||
);
|
||||
final bypassGlobalProxy = useState(
|
||||
initialContainer.metadata.bypassGlobalProxy,
|
||||
);
|
||||
final assignedSites = useState(initialContainer.metadata.assignedSites);
|
||||
final isPinned = useState(initialContainer.isPinned);
|
||||
|
||||
@@ -122,6 +125,10 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
clearDataOnExit:
|
||||
clearDataOnExit.value && contextualIdentity.value != null,
|
||||
excludeFromIndex: excludeFromIndex.value,
|
||||
bypassGlobalProxy:
|
||||
contextualIdentity.value != null &&
|
||||
proxyConnectionId.value == null &&
|
||||
bypassGlobalProxy.value,
|
||||
assignedSites: assignedSites.value,
|
||||
),
|
||||
);
|
||||
@@ -242,6 +249,8 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
final assignedSiteCount = assignedSites.value?.length ?? 0;
|
||||
final canPickProxy =
|
||||
_mode == _DialogMode.create || contextualIdentity.value != null;
|
||||
final canBypassGlobalProxy =
|
||||
contextualIdentity.value != null && proxyConnectionId.value == null;
|
||||
|
||||
return PopScope(
|
||||
canPop: container == comparison,
|
||||
@@ -404,6 +413,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
|
||||
if (!value) {
|
||||
proxyConnectionId.value = null;
|
||||
bypassGlobalProxy.value = false;
|
||||
}
|
||||
|
||||
if (!value && clearDataOnExit.value) {
|
||||
@@ -473,14 +483,31 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
proxyConnectionId.value = null;
|
||||
if (createdTemporaryIdentity) {
|
||||
contextualIdentity.value = null;
|
||||
bypassGlobalProxy.value = false;
|
||||
}
|
||||
case _ProxyPickerSelected(:final id):
|
||||
proxyConnectionId.value = id;
|
||||
bypassGlobalProxy.value = false;
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
const Divider(height: 1, indent: 56),
|
||||
SwitchListTile.adaptive(
|
||||
value:
|
||||
canBypassGlobalProxy && bypassGlobalProxy.value,
|
||||
title: const Text('Bypass Global Proxy'),
|
||||
subtitle: const Text(
|
||||
'Use the normal connection for this container when global routing is enabled',
|
||||
),
|
||||
secondary: const Icon(Icons.public),
|
||||
onChanged: canBypassGlobalProxy
|
||||
? (value) {
|
||||
bypassGlobalProxy.value = value;
|
||||
}
|
||||
: null,
|
||||
),
|
||||
const Divider(height: 1, indent: 56),
|
||||
SwitchListTile.adaptive(
|
||||
value: clearDataOnExit.value,
|
||||
title: const Text('Clear Data on Exit'),
|
||||
@@ -661,6 +688,7 @@ class _ProxyConnectionPickerSheet extends StatelessWidget {
|
||||
|
||||
return SafeArea(
|
||||
child: RadioGroup<ProxyConnectionId?>(
|
||||
groupValue: selectedProxyConnectionId,
|
||||
onChanged: (value) {
|
||||
Navigator.pop(
|
||||
context,
|
||||
|
||||
+7
@@ -266,6 +266,13 @@ class _ContainerCard extends HookConsumerWidget {
|
||||
isLoading: proxyOptionsLoading,
|
||||
),
|
||||
),
|
||||
if (container.metadata.proxyConnectionId ==
|
||||
null &&
|
||||
container.metadata.bypassGlobalProxy)
|
||||
const _ContainerInfoChip(
|
||||
icon: Icons.public,
|
||||
label: 'Direct',
|
||||
),
|
||||
if (container.metadata.clearDataOnExit)
|
||||
const _ContainerInfoChip(
|
||||
icon: Icons.cleaning_services_outlined,
|
||||
|
||||
+7
@@ -322,6 +322,13 @@ class _SelectionContainerCard extends ConsumerWidget {
|
||||
isLoading: proxyOptionsLoading,
|
||||
),
|
||||
),
|
||||
if (container.metadata.proxyConnectionId ==
|
||||
null &&
|
||||
container.metadata.bypassGlobalProxy)
|
||||
const _SelectionInfoChip(
|
||||
icon: Icons.public,
|
||||
label: 'Direct',
|
||||
),
|
||||
if (container.metadata.clearDataOnExit)
|
||||
const _SelectionInfoChip(
|
||||
icon: Icons.cleaning_services_outlined,
|
||||
|
||||
@@ -83,6 +83,16 @@ class ContainerProxyRepository extends _$ContainerProxyRepository {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setContainerDirectConnection(
|
||||
String contextId, {
|
||||
required String scopeId,
|
||||
}) {
|
||||
return _runLocked(
|
||||
body: () =>
|
||||
_service.setContainerDirectConnection(contextId, scopeId: scopeId),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearContainerProxy(String contextId) {
|
||||
return _runLocked(body: () => _service.clearContainerProxy(contextId));
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ final class ContainerProxyRepositoryProvider
|
||||
}
|
||||
|
||||
String _$containerProxyRepositoryHash() =>
|
||||
r'08cd408a3ae96c859ed5c9d56d61cf7e6514415f';
|
||||
r'ec3ec41b45b991623518f0aceb6ee7b21abb9112';
|
||||
|
||||
abstract class _$ContainerProxyRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -104,7 +104,7 @@ class _RegularTabsModeSection extends ConsumerWidget {
|
||||
value: ProxyRegularTabRoutingMode.all,
|
||||
title: Text('Global Routing'),
|
||||
subtitle: Text(
|
||||
'Route every regular tab through the selected proxy.',
|
||||
'Route regular tabs through the selected proxy unless a container bypasses it.',
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -27,6 +27,8 @@ import 'package:weblibre/extensions/uri.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/proxy/data/proxy_connection.dart';
|
||||
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
||||
@@ -35,6 +37,20 @@ import 'package:weblibre/features/user/domain/repositories/proxy_routing_setting
|
||||
|
||||
part 'website_title.g.dart';
|
||||
|
||||
sealed class _PageInfoProxyAssignment {
|
||||
const _PageInfoProxyAssignment();
|
||||
}
|
||||
|
||||
final class _PageInfoDirectAssignment extends _PageInfoProxyAssignment {
|
||||
const _PageInfoDirectAssignment();
|
||||
}
|
||||
|
||||
final class _PageInfoExplicitProxyAssignment extends _PageInfoProxyAssignment {
|
||||
final ProxyConnectionId proxyConnectionId;
|
||||
|
||||
const _PageInfoExplicitProxyAssignment(this.proxyConnectionId);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
class CompletePageInfo extends _$CompletePageInfo {
|
||||
@override
|
||||
@@ -91,12 +107,34 @@ Future<WebPageInfo> pageInfo(
|
||||
proxyRoutingSettingsWithDefaultsProvider,
|
||||
);
|
||||
|
||||
if (containerData?.metadata.proxyConnectionId is TorProxyConnectionId ||
|
||||
(tabState.tabMode is! PrivateTabMode &&
|
||||
proxyRoutingSettings.regularTabsMode ==
|
||||
ProxyRegularTabRoutingMode.all &&
|
||||
proxyRoutingSettings.regularTabsProxyConnectionId
|
||||
is TorProxyConnectionId) ||
|
||||
final isolatedAssignment = tabState.isolationContextId != null
|
||||
? await _pageInfoAssignmentForIsolationContext(
|
||||
ref,
|
||||
tabState.isolationContextId!,
|
||||
)
|
||||
: null;
|
||||
|
||||
final effectiveContainerProxyConnectionId = switch (isolatedAssignment) {
|
||||
_PageInfoExplicitProxyAssignment(:final proxyConnectionId) =>
|
||||
proxyConnectionId,
|
||||
_PageInfoDirectAssignment() => null,
|
||||
null => containerData?.metadata.proxyConnectionId,
|
||||
};
|
||||
final bypassesGlobalProxy =
|
||||
isolatedAssignment is _PageInfoDirectAssignment ||
|
||||
(isolatedAssignment == null &&
|
||||
containerData?.metadata.bypassGlobalProxy == true);
|
||||
final usesGlobalRegularTor =
|
||||
tabState.tabMode is! PrivateTabMode &&
|
||||
effectiveContainerProxyConnectionId == null &&
|
||||
!bypassesGlobalProxy &&
|
||||
proxyRoutingSettings.regularTabsMode ==
|
||||
ProxyRegularTabRoutingMode.all &&
|
||||
proxyRoutingSettings.regularTabsProxyConnectionId
|
||||
is TorProxyConnectionId;
|
||||
|
||||
if (effectiveContainerProxyConnectionId is TorProxyConnectionId ||
|
||||
usesGlobalRegularTor ||
|
||||
(tabState.tabMode is PrivateTabMode &&
|
||||
proxyRoutingSettings.privateTabsProxyConnectionId
|
||||
is TorProxyConnectionId)) {
|
||||
@@ -125,6 +163,51 @@ Future<WebPageInfo> pageInfo(
|
||||
return result.value;
|
||||
}
|
||||
|
||||
Future<_PageInfoProxyAssignment?> _pageInfoAssignmentForIsolationContext(
|
||||
Ref ref,
|
||||
String contextId,
|
||||
) async {
|
||||
final db = ref.read(tabDatabaseProvider);
|
||||
final pairs = await db.tabDao.isolatedContextContainerPairs().get();
|
||||
final containerIds = pairs
|
||||
.where((pair) => pair.isolationContextId == contextId)
|
||||
.map((pair) => pair.containerId)
|
||||
.nonNulls
|
||||
.toSet();
|
||||
|
||||
if (containerIds.isEmpty) return null;
|
||||
|
||||
final containers = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getAllContainersWithCount();
|
||||
final matchedContainers = containers.where(
|
||||
(container) => containerIds.contains(container.id),
|
||||
);
|
||||
final proxyIds =
|
||||
matchedContainers
|
||||
.map((container) => container.metadata.proxyConnectionId?.encode())
|
||||
.nonNulls
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort();
|
||||
|
||||
if (proxyIds.isNotEmpty) {
|
||||
final proxyConnectionId = ProxyConnectionId.decode(proxyIds.first);
|
||||
return proxyConnectionId != null
|
||||
? _PageInfoExplicitProxyAssignment(proxyConnectionId)
|
||||
: null;
|
||||
}
|
||||
final matchedContainerList = matchedContainers.toList();
|
||||
if (matchedContainerList.isNotEmpty &&
|
||||
matchedContainerList.every(
|
||||
(container) => container.metadata.bypassGlobalProxy,
|
||||
)) {
|
||||
return const _PageInfoDirectAssignment();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
AsyncValue<EquatableValue<Set<Uri>?>> websiteFeedProvider(
|
||||
Ref ref,
|
||||
|
||||
@@ -162,7 +162,7 @@ final class PageInfoProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$pageInfoHash() => r'1dedc23d2420c950c7cec57500e99c5c221ed6e2';
|
||||
String _$pageInfoHash() => r'680a8f0f0101c86946b4839d30d6f55307312cce';
|
||||
|
||||
final class PageInfoFamily extends $Family
|
||||
with
|
||||
|
||||
+170
-1
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_tor/flutter_tor.dart';
|
||||
@@ -9,6 +10,8 @@ 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/entities/tab_mode.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.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';
|
||||
@@ -82,12 +85,168 @@ void main() {
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test('bypassed containers sync explicit direct routing', () async {
|
||||
final bypassedContainer = _container(
|
||||
id: 'container-1',
|
||||
contextId: 'context-a',
|
||||
bypassGlobalProxy: true,
|
||||
);
|
||||
final db = TabDatabase(
|
||||
NativeDatabase.memory(
|
||||
setup: (database) {
|
||||
registerLexorankFunctions(database);
|
||||
registerUrlFunctions(database);
|
||||
},
|
||||
),
|
||||
);
|
||||
final containerProxyRepository = _FakeContainerProxyRepository();
|
||||
final containerRepository = _FakeContainerRepository([bypassedContainer]);
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
tabDatabaseProvider.overrideWith((ref) => db),
|
||||
containerProxyRepositoryProvider.overrideWith(
|
||||
() => containerProxyRepository,
|
||||
),
|
||||
containerRepositoryProvider.overrideWith(() => containerRepository),
|
||||
torProxyServiceProvider.overrideWith(_FakeTorProxyService.new),
|
||||
proxyRoutingSettingsWithDefaultsProvider.overrideWith(
|
||||
(ref) => ProxyRoutingSettings.withDefaults(
|
||||
regularTabsMode: ProxyRegularTabRoutingMode.all,
|
||||
regularTabsProxyConnectionId: const TorProxyConnectionId(),
|
||||
),
|
||||
),
|
||||
watchContainersWithCountProvider.overrideWith(
|
||||
(ref) => Stream.value([bypassedContainer]),
|
||||
),
|
||||
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(containerProxyRepository.setContainerDirectConnectionCalls, [
|
||||
('context-a', 'context-a'),
|
||||
]);
|
||||
expect(containerProxyRepository.setContainerProxyCalls, [
|
||||
('general', const TorProxyConnectionId().encode()),
|
||||
]);
|
||||
});
|
||||
|
||||
test(
|
||||
'mixed bypassed and inherited isolated containers do not bypass globally',
|
||||
() async {
|
||||
final bypassedContainer = _container(
|
||||
id: 'container-direct',
|
||||
contextId: 'context-direct',
|
||||
bypassGlobalProxy: true,
|
||||
);
|
||||
final inheritedContainer = _container(
|
||||
id: 'container-inherit',
|
||||
contextId: 'context-inherit',
|
||||
);
|
||||
final db = TabDatabase(
|
||||
NativeDatabase.memory(
|
||||
setup: (database) {
|
||||
registerLexorankFunctions(database);
|
||||
registerUrlFunctions(database);
|
||||
},
|
||||
),
|
||||
);
|
||||
await db.containerDao.addContainer(bypassedContainer);
|
||||
await db.containerDao.addContainer(inheritedContainer);
|
||||
await db.tabDao.insertTab(
|
||||
'tab-direct',
|
||||
source: TabSource.manual,
|
||||
parentId: const Value(null),
|
||||
containerId: const Value('container-direct'),
|
||||
tabMode: Value(TabMode.isolated('isolation-a')),
|
||||
);
|
||||
await db.tabDao.insertTab(
|
||||
'tab-inherit',
|
||||
source: TabSource.manual,
|
||||
parentId: const Value(null),
|
||||
containerId: const Value('container-inherit'),
|
||||
tabMode: Value(TabMode.isolated('isolation-a')),
|
||||
);
|
||||
final containerProxyRepository = _FakeContainerProxyRepository();
|
||||
final containers = [bypassedContainer, inheritedContainer];
|
||||
final containerRepository = _FakeContainerRepository(containers);
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
tabDatabaseProvider.overrideWith((ref) => db),
|
||||
containerProxyRepositoryProvider.overrideWith(
|
||||
() => containerProxyRepository,
|
||||
),
|
||||
containerRepositoryProvider.overrideWith(() => containerRepository),
|
||||
torProxyServiceProvider.overrideWith(_FakeTorProxyService.new),
|
||||
proxyRoutingSettingsWithDefaultsProvider.overrideWith(
|
||||
(ref) => ProxyRoutingSettings.withDefaults(
|
||||
regularTabsMode: ProxyRegularTabRoutingMode.all,
|
||||
regularTabsProxyConnectionId: const TorProxyConnectionId(),
|
||||
),
|
||||
),
|
||||
watchContainersWithCountProvider.overrideWith(
|
||||
(ref) => Stream.value(containers),
|
||||
),
|
||||
watchAllAssignedSitesProvider.overrideWith(
|
||||
(ref) => Stream.value(const <SiteAssignment>[]),
|
||||
),
|
||||
watchIsolatedContextContainerMapProvider.overrideWith(
|
||||
(ref) => Stream.value(const {
|
||||
'isolation-a': {'container-direct', 'container-inherit'},
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(() async {
|
||||
container.dispose();
|
||||
await db.close();
|
||||
});
|
||||
|
||||
final subscription = container.listen<void>(
|
||||
proxySettingsReplicationProvider,
|
||||
(previous, next) {},
|
||||
fireImmediately: true,
|
||||
);
|
||||
addTearDown(subscription.close);
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(containerProxyRepository.setContainerDirectConnectionCalls, [
|
||||
('context-direct', 'context-direct'),
|
||||
]);
|
||||
expect(
|
||||
containerProxyRepository.setContainerProxyCalls,
|
||||
contains(('general', const TorProxyConnectionId().encode())),
|
||||
);
|
||||
expect(
|
||||
containerProxyRepository.setContainerDirectConnectionCalls,
|
||||
isNot(contains(('isolation-a', 'context-direct'))),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ContainerDataWithCount _container({
|
||||
required String id,
|
||||
required String contextId,
|
||||
required ProxyConnectionId proxyConnectionId,
|
||||
ProxyConnectionId? proxyConnectionId,
|
||||
bool bypassGlobalProxy = false,
|
||||
}) {
|
||||
return ContainerDataWithCount(
|
||||
id: id,
|
||||
@@ -97,6 +256,7 @@ ContainerDataWithCount _container({
|
||||
metadata: ContainerMetadata.withDefaults(
|
||||
contextualIdentity: contextId,
|
||||
proxyConnectionId: proxyConnectionId,
|
||||
bypassGlobalProxy: bypassGlobalProxy,
|
||||
),
|
||||
tabCount: 0,
|
||||
);
|
||||
@@ -104,6 +264,7 @@ ContainerDataWithCount _container({
|
||||
|
||||
class _FakeContainerProxyRepository extends ContainerProxyRepository {
|
||||
final setContainerProxyCalls = <(String, String)>[];
|
||||
final setContainerDirectConnectionCalls = <(String, String)>[];
|
||||
|
||||
@override
|
||||
Future<void> setTorProxyPort(int? port) async {}
|
||||
@@ -116,6 +277,14 @@ class _FakeContainerProxyRepository extends ContainerProxyRepository {
|
||||
setContainerProxyCalls.add((contextId, proxyId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setContainerDirectConnection(
|
||||
String contextId, {
|
||||
required String scopeId,
|
||||
}) async {
|
||||
setContainerDirectConnectionCalls.add((contextId, scopeId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearContainerProxy(String contextId) async {}
|
||||
|
||||
|
||||
+3
@@ -9,6 +9,7 @@ void main() {
|
||||
final metadata = ContainerMetadata.withDefaults(
|
||||
proxyConnectionId: const SingboxProxyConnectionId('profile-1'),
|
||||
clearDataOnExit: true,
|
||||
bypassGlobalProxy: true,
|
||||
);
|
||||
|
||||
final json = metadata.toJson();
|
||||
@@ -19,7 +20,9 @@ void main() {
|
||||
const SingboxProxyConnectionId('profile-1').encode(),
|
||||
);
|
||||
expect(json['clearDataOnExit'], isTrue);
|
||||
expect(json['bypassGlobalProxy'], isTrue);
|
||||
expect(restored.proxyConnectionId, metadata.proxyConnectionId);
|
||||
expect(restored.bypassGlobalProxy, isTrue);
|
||||
expect(restored.usesTorProxy, isFalse);
|
||||
});
|
||||
|
||||
|
||||
+10
@@ -44,6 +44,16 @@ class GeckoContainerProxyApiImpl : GeckoContainerProxyApi {
|
||||
)
|
||||
}
|
||||
|
||||
override fun setContainerDirectConnection(contextId: String, scopeId: String) {
|
||||
ContainerProxyFeature.scheduleRequest(
|
||||
"setContainerDirectConnection",
|
||||
JSONObject().apply {
|
||||
put("contextId", contextId)
|
||||
put("scopeId", scopeId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun clearContainerProxy(contextId: String) {
|
||||
ContainerProxyFeature.scheduleRequest("clearContainerProxy", contextId)
|
||||
}
|
||||
|
||||
+20
@@ -8412,6 +8412,7 @@ interface GeckoContainerProxyApi {
|
||||
fun upsertProxy(proxy: GeckoProxySettings)
|
||||
fun removeProxy(proxyId: String)
|
||||
fun setContainerProxy(contextId: String, proxyId: String)
|
||||
fun setContainerDirectConnection(contextId: String, scopeId: String)
|
||||
fun clearContainerProxy(contextId: String)
|
||||
fun removeContainerProxyRelation(contextId: String, proxyId: String)
|
||||
fun setSiteAssignments(assignments: Map<String, String>)
|
||||
@@ -8535,6 +8536,25 @@ interface GeckoContainerProxyApi {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setContainerDirectConnection$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val contextIdArg = args[0] as String
|
||||
val scopeIdArg = args[1] as String
|
||||
val wrapped: List<Any?> = try {
|
||||
api.setContainerDirectConnection(contextIdArg, scopeIdArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
GeckoPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.clearContainerProxy$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
|
||||
+10
@@ -14,6 +14,7 @@ interface Message {
|
||||
'upsertProxy' |
|
||||
'removeProxy' |
|
||||
'setContainerProxy' |
|
||||
'setContainerDirectConnection' |
|
||||
'clearContainerProxy' |
|
||||
'removeContainerProxyRelation' |
|
||||
'healthcheck' |
|
||||
@@ -69,6 +70,15 @@ port.onMessage.addListener((raw: unknown): void => {
|
||||
store.setContainerProxyRelation(message.args.contextId, message.args.proxyId)
|
||||
console.log('set container relation ' + message.args.contextId + ' -> ' + message.args.proxyId)
|
||||
break
|
||||
case "setContainerDirectConnection":
|
||||
if (typeof message.args === 'string') {
|
||||
store.setContainerDirectRelation(message.args)
|
||||
console.log('set container direct relation ' + message.args)
|
||||
} else {
|
||||
store.setContainerDirectRelation(message.args.contextId, message.args.scopeId)
|
||||
console.log('set container direct relation ' + message.args.contextId + ' scoped to ' + message.args.scopeId)
|
||||
}
|
||||
break
|
||||
case "clearContainerProxy":
|
||||
store.clearContainerProxyRelation(message.args)
|
||||
console.log('cleared container relation ' + message.args)
|
||||
|
||||
@@ -84,6 +84,7 @@ interface WildcardAssignment {
|
||||
export class Store {
|
||||
private proxies: ProxyDao[] = []
|
||||
private relations: { [key: string]: string[] } = {}
|
||||
private directRelationScopes: { [key: string]: string } = {}
|
||||
|
||||
private siteAssignments: Map<string, string> = new Map<string, string>()
|
||||
private wildcardAssignments: WildcardAssignment[] = []
|
||||
@@ -147,15 +148,35 @@ export class Store {
|
||||
return this.lookupAssignment(uri) !== undefined
|
||||
}
|
||||
|
||||
private hasRelation(contextId: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(this.relations, contextId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the effective proxy relation for a context ID, mirroring the
|
||||
* fallback logic in getProxiesForContainer: explicit relation first,
|
||||
* then 'general' for non-private contexts, then empty.
|
||||
* Returns the effective proxy relation for a context ID, preserving the
|
||||
* difference between "no explicit relation" (undefined, may inherit) and
|
||||
* "explicit direct connection" ([]).
|
||||
*/
|
||||
private getEffectiveRelation(contextId: string): string[] {
|
||||
return this.relations[contextId]
|
||||
?? ((contextId !== 'private') ? this.relations['general'] : undefined)
|
||||
?? [];
|
||||
private getEffectiveRelation(contextId: string): string[] | undefined {
|
||||
if (this.hasRelation(contextId)) {
|
||||
return this.relations[contextId]
|
||||
}
|
||||
if (contextId !== 'private' && this.hasRelation('general')) {
|
||||
return this.relations['general']
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
private getEffectiveDirectScope(contextId: string): string | undefined {
|
||||
if (this.hasRelation(contextId) && this.relations[contextId].length === 0) {
|
||||
return this.directRelationScopes[contextId] ?? contextId
|
||||
}
|
||||
if (contextId !== 'private' &&
|
||||
this.hasRelation('general') &&
|
||||
this.relations['general'].length === 0) {
|
||||
return this.directRelationScopes['general'] ?? 'general'
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
isSiteOriginInSameContext(uri: URL, contextId: string): boolean {
|
||||
@@ -169,10 +190,19 @@ export class Store {
|
||||
const assignedRelation = this.getEffectiveRelation(assignedContextId);
|
||||
const currentRelation = this.getEffectiveRelation(contextId);
|
||||
|
||||
// Only treat as equivalent if both have actual proxy relations —
|
||||
// empty relations mean no proxy, and different non-proxied contexts
|
||||
// should not be considered equivalent.
|
||||
if (assignedRelation.length > 0 &&
|
||||
const assignedDirectScope = this.getEffectiveDirectScope(assignedContextId)
|
||||
const currentDirectScope = this.getEffectiveDirectScope(contextId)
|
||||
if (assignedDirectScope !== undefined || currentDirectScope !== undefined) {
|
||||
return assignedDirectScope !== undefined &&
|
||||
assignedDirectScope === currentDirectScope
|
||||
}
|
||||
|
||||
// Treat proxy-routed contexts as equivalent only when both resolve to an
|
||||
// explicit non-direct relation. Direct relations are scoped above so two
|
||||
// unrelated bypassed containers do not collapse into the same context.
|
||||
if (assignedRelation !== undefined &&
|
||||
currentRelation !== undefined &&
|
||||
assignedRelation.length > 0 &&
|
||||
assignedRelation.length === currentRelation.length &&
|
||||
assignedRelation.every((id, i) => id === currentRelation[i])) {
|
||||
return true;
|
||||
@@ -222,10 +252,17 @@ export class Store {
|
||||
|
||||
setContainerProxyRelation(cookieStoreId: string, proxyId: string): void {
|
||||
this.relations[cookieStoreId] = [proxyId]
|
||||
delete this.directRelationScopes[cookieStoreId]
|
||||
}
|
||||
|
||||
setContainerDirectRelation(cookieStoreId: string, scopeId: string = cookieStoreId): void {
|
||||
this.relations[cookieStoreId] = []
|
||||
this.directRelationScopes[cookieStoreId] = scopeId
|
||||
}
|
||||
|
||||
clearContainerProxyRelation(cookieStoreId: string): void {
|
||||
delete this.relations[cookieStoreId]
|
||||
delete this.directRelationScopes[cookieStoreId]
|
||||
}
|
||||
|
||||
removeContainerProxyRelation(cookieStoreId: string, proxyId: string): void {
|
||||
|
||||
+30
@@ -172,6 +172,17 @@ describe('Store', () => {
|
||||
|
||||
expect(result).to.be.deep.equal([])
|
||||
})
|
||||
|
||||
it('should let an explicit direct relation bypass the general relation', () => {
|
||||
const isolatedStore = new Store()
|
||||
isolatedStore.putProxy(someProxyWith('global-proxy'))
|
||||
isolatedStore.setContainerProxyRelation('general', 'global-proxy')
|
||||
isolatedStore.setContainerDirectRelation('container1')
|
||||
|
||||
const result = isolatedStore.getProxiesForContainer('container1')
|
||||
|
||||
expect(result).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe('wildcard site assignments', function () {
|
||||
@@ -247,5 +258,24 @@ describe('Store', () => {
|
||||
const result = store.isSiteOriginInSameContext(new URL('https://blocked.example/another'), 'iso1_blocked')
|
||||
expect(result).to.be.equal(false)
|
||||
})
|
||||
|
||||
it('should allow direct aliases scoped to the assigned container', async () => {
|
||||
store.setSiteAssignments(new Map([['https://direct.example/path', 'container_direct']]))
|
||||
await store.setContainerProxyRelation('general', 'proxy_global')
|
||||
await store.setContainerDirectRelation('container_direct')
|
||||
await store.setContainerDirectRelation('iso1_direct', 'container_direct')
|
||||
|
||||
const result = store.isSiteOriginInSameContext(new URL('https://direct.example/another'), 'iso1_direct')
|
||||
expect(result).to.be.equal(true)
|
||||
})
|
||||
|
||||
it('should not allow unrelated direct containers as equivalent', async () => {
|
||||
store.setSiteAssignments(new Map([['https://direct.example/path', 'container_direct']]))
|
||||
await store.setContainerDirectRelation('container_direct')
|
||||
await store.setContainerDirectRelation('other_direct')
|
||||
|
||||
const result = store.isSiteOriginInSameContext(new URL('https://direct.example/another'), 'other_direct')
|
||||
expect(result).to.be.equal(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,6 +36,13 @@ class GeckoContainerProxyService {
|
||||
return _apiInstance.setContainerProxy(contextId, proxyId);
|
||||
}
|
||||
|
||||
Future<void> setContainerDirectConnection(
|
||||
String contextId, {
|
||||
required String scopeId,
|
||||
}) {
|
||||
return _apiInstance.setContainerDirectConnection(contextId, scopeId);
|
||||
}
|
||||
|
||||
Future<void> clearContainerProxy(String contextId) {
|
||||
return _apiInstance.clearContainerProxy(contextId);
|
||||
}
|
||||
|
||||
@@ -8799,6 +8799,29 @@ class GeckoContainerProxyApi {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setContainerDirectConnection(
|
||||
String contextId,
|
||||
String scopeId,
|
||||
) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setContainerDirectConnection$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[contextId, scopeId],
|
||||
);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearContainerProxy(String contextId) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.clearContainerProxy$pigeonVar_messageChannelSuffix';
|
||||
|
||||
@@ -1888,6 +1888,7 @@ abstract class GeckoContainerProxyApi {
|
||||
void upsertProxy(GeckoProxySettings proxy);
|
||||
void removeProxy(String proxyId);
|
||||
void setContainerProxy(String contextId, String proxyId);
|
||||
void setContainerDirectConnection(String contextId, String scopeId);
|
||||
void clearContainerProxy(String contextId);
|
||||
void removeContainerProxyRelation(String contextId, String proxyId);
|
||||
void setSiteAssignments(Map<String, String> assignments);
|
||||
|
||||
Reference in New Issue
Block a user