container strict mode feature
This commit is contained in:
@@ -900,6 +900,14 @@ class TabRepository extends _$TabRepository {
|
|||||||
|
|
||||||
final containerSiteAssignementSub = eventSerivce.siteAssignementEvent.listen(
|
final containerSiteAssignementSub = eventSerivce.siteAssignementEvent.listen(
|
||||||
(event) async {
|
(event) async {
|
||||||
|
// Strict-mode blocks have no destination container to re-open into; the
|
||||||
|
// navigation was already cancelled natively and the user is notified via
|
||||||
|
// a snackbar (see the strict-block listener in the app shell). Nothing
|
||||||
|
// to reconcile here.
|
||||||
|
if (event.strict) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final tabId = event.tabId;
|
final tabId = event.tabId;
|
||||||
if (tabId != null) {
|
if (tabId != null) {
|
||||||
final tabState = ref.read(tabStatesProvider)[tabId];
|
final tabState = ref.read(tabStatesProvider)[tabId];
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'982b80b8ea7c694958bc10cc7e8e15310af6a238';
|
String _$tabRepositoryHash() => r'dd2f9c776be1d5437141e94dbd0b7ef382093b63';
|
||||||
|
|
||||||
abstract class _$TabRepository extends $Notifier<void> {
|
abstract class _$TabRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+8
@@ -289,6 +289,14 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ref.listen(watchStrictContextAssignmentsProvider, (previous, next) async {
|
||||||
|
if (next.hasValue) {
|
||||||
|
await ref
|
||||||
|
.read(containerProxyRepositoryProvider.notifier)
|
||||||
|
.setStrictContexts(next.requireValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
watchIsolatedContextContainerMapProvider.select(
|
watchIsolatedContextContainerMapProvider.select(
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ final class ProxySettingsReplicationProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$proxySettingsReplicationHash() =>
|
String _$proxySettingsReplicationHash() =>
|
||||||
r'cdb624b7f7806a7ce4218da350253ed3d29986d0';
|
r'69787c85c94ff165e3eeb0f0a3f3fc83e88a1b83';
|
||||||
|
|
||||||
abstract class _$ProxySettingsReplication extends $Notifier<void> {
|
abstract class _$ProxySettingsReplication extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -259,6 +259,10 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
|||||||
return db.definitionsDrift.containersToClearOnExit();
|
return db.definitionsDrift.containersToClearOnExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Selectable<StrictContextAssignmentsResult> strictContextAssignments() {
|
||||||
|
return db.definitionsDrift.strictContextAssignments();
|
||||||
|
}
|
||||||
|
|
||||||
SingleOrNullSelectable<ContainerData> getContainerByContextualIdentity(
|
SingleOrNullSelectable<ContainerData> getContainerByContextualIdentity(
|
||||||
String contextId,
|
String contextId,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -969,6 +969,40 @@ containersToClearOnExit:
|
|||||||
json_extract(container.metadata, '$.clearDataOnExit') = 1
|
json_extract(container.metadata, '$.clearDataOnExit') = 1
|
||||||
AND container.metadata ->> '$.contextualIdentity' IS NOT NULL;
|
AND container.metadata ->> '$.contextualIdentity' IS NOT NULL;
|
||||||
|
|
||||||
|
-- Strict-mode enforcement map for the container-proxy extension. Each row maps
|
||||||
|
-- a Gecko cookie-store context (`context_id`) that must be enforced to the
|
||||||
|
-- container's base contextualIdentity (`assignment_context_id`) that its site
|
||||||
|
-- assignments are keyed on. In strict mode a navigation is only allowed when
|
||||||
|
-- the target origin is assigned to `assignment_context_id` exactly (no proxy
|
||||||
|
-- equivalence).
|
||||||
|
--
|
||||||
|
-- Two sources, unioned:
|
||||||
|
-- 1. The strict container's own base context (context_id == assignment).
|
||||||
|
-- 2. Every isolation context in use by an isolated tab (tab_mode = 2) whose
|
||||||
|
-- container is strict: those tabs load under `isolation_context_id` as
|
||||||
|
-- their cookie store, but their assignments live on the container's base
|
||||||
|
-- context, so they map isolation_context_id -> base context.
|
||||||
|
-- Null contextualIdentity is excluded: strictness needs a Gecko contextId.
|
||||||
|
strictContextAssignments:
|
||||||
|
SELECT
|
||||||
|
container.metadata ->> '$.contextualIdentity' AS context_id,
|
||||||
|
container.metadata ->> '$.contextualIdentity' AS assignment_context_id
|
||||||
|
FROM container
|
||||||
|
WHERE
|
||||||
|
json_extract(container.metadata, '$.strictMode') = 1
|
||||||
|
AND container.metadata ->> '$.contextualIdentity' IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT DISTINCT
|
||||||
|
t.isolation_context_id AS context_id,
|
||||||
|
c.metadata ->> '$.contextualIdentity' AS assignment_context_id
|
||||||
|
FROM tab t
|
||||||
|
INNER JOIN container c ON c.id = t.container_id
|
||||||
|
WHERE
|
||||||
|
t.tab_mode = 2
|
||||||
|
AND t.isolation_context_id IS NOT NULL
|
||||||
|
AND json_extract(c.metadata, '$.strictMode') = 1
|
||||||
|
AND c.metadata ->> '$.contextualIdentity' IS NOT NULL;
|
||||||
|
|
||||||
tabsInIsolationGroup:
|
tabsInIsolationGroup:
|
||||||
SELECT COUNT(*) AS count FROM tab WHERE isolation_context_id = :contextId;
|
SELECT COUNT(*) AS count FROM tab WHERE isolation_context_id = :contextId;
|
||||||
|
|
||||||
|
|||||||
+19
@@ -6695,6 +6695,19 @@ class DefinitionsDrift extends i9.ModularAccessor {
|
|||||||
).map((i0.QueryRow row) => row.readNullable<String>('contextual_identity'));
|
).map((i0.QueryRow row) => row.readNullable<String>('contextual_identity'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i0.Selectable<StrictContextAssignmentsResult> strictContextAssignments() {
|
||||||
|
return customSelect(
|
||||||
|
'SELECT container.metadata ->> \'\$.contextualIdentity\' AS context_id, container.metadata ->> \'\$.contextualIdentity\' AS assignment_context_id FROM container WHERE json_extract(container.metadata, \'\$.strictMode\') = 1 AND container.metadata ->> \'\$.contextualIdentity\' IS NOT NULL UNION SELECT DISTINCT t.isolation_context_id AS context_id, c.metadata ->> \'\$.contextualIdentity\' AS assignment_context_id FROM tab AS t INNER JOIN container AS c ON c.id = t.container_id WHERE t.tab_mode = 2 AND t.isolation_context_id IS NOT NULL AND json_extract(c.metadata, \'\$.strictMode\') = 1 AND c.metadata ->> \'\$.contextualIdentity\' IS NOT NULL',
|
||||||
|
variables: [],
|
||||||
|
readsFrom: {container, tab},
|
||||||
|
).map(
|
||||||
|
(i0.QueryRow row) => StrictContextAssignmentsResult(
|
||||||
|
contextId: row.readNullable<String>('context_id'),
|
||||||
|
assignmentContextId: row.readNullable<String>('assignment_context_id'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
i0.Selectable<int> tabsInIsolationGroup({String? contextId}) {
|
i0.Selectable<int> tabsInIsolationGroup({String? contextId}) {
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'SELECT COUNT(*) AS count FROM tab WHERE isolation_context_id = ?1',
|
'SELECT COUNT(*) AS count FROM tab WHERE isolation_context_id = ?1',
|
||||||
@@ -6789,6 +6802,12 @@ class ContainerIdsByContextualIdentitiesResult {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StrictContextAssignmentsResult {
|
||||||
|
final String? contextId;
|
||||||
|
final String? assignmentContextId;
|
||||||
|
StrictContextAssignmentsResult({this.contextId, this.assignmentContextId});
|
||||||
|
}
|
||||||
|
|
||||||
class IsolatedContextContainerPairsResult {
|
class IsolatedContextContainerPairsResult {
|
||||||
final String? isolationContextId;
|
final String? isolationContextId;
|
||||||
final String? containerId;
|
final String? containerId;
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ class ContainerMetadata with FastEquatable {
|
|||||||
|
|
||||||
final List<Uri>? assignedSites;
|
final List<Uri>? assignedSites;
|
||||||
|
|
||||||
|
// When true, tabs in this container may only load origins listed in
|
||||||
|
// [assignedSites]; any other top-level navigation is blocked. Read on the
|
||||||
|
// native side via `json_extract(metadata, '$.strictMode')` (see the
|
||||||
|
// `strictContextAssignments` query in definitions.drift) and pushed to the
|
||||||
|
// container-proxy web extension. Requires a Gecko contextId — the extension
|
||||||
|
// keys strictness on the tab's cookieStoreId — so it is normalized to false
|
||||||
|
// when [contextualIdentity] is null (mirrors [excludeFromHistory]).
|
||||||
|
@JsonKey(defaultValue: false)
|
||||||
|
final bool strictMode;
|
||||||
|
|
||||||
ContainerMetadata({
|
ContainerMetadata({
|
||||||
required this.iconData,
|
required this.iconData,
|
||||||
required this.contextualIdentity,
|
required this.contextualIdentity,
|
||||||
@@ -83,6 +93,7 @@ class ContainerMetadata with FastEquatable {
|
|||||||
required this.bypassGlobalProxy,
|
required this.bypassGlobalProxy,
|
||||||
required this.useCustomColor,
|
required this.useCustomColor,
|
||||||
required this.assignedSites,
|
required this.assignedSites,
|
||||||
|
required this.strictMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
ContainerMetadata.withDefaults({
|
ContainerMetadata.withDefaults({
|
||||||
@@ -95,6 +106,7 @@ class ContainerMetadata with FastEquatable {
|
|||||||
bool? bypassGlobalProxy,
|
bool? bypassGlobalProxy,
|
||||||
bool? useCustomColor,
|
bool? useCustomColor,
|
||||||
List<Uri>? assignedSites,
|
List<Uri>? assignedSites,
|
||||||
|
bool? strictMode,
|
||||||
}) : this(
|
}) : this(
|
||||||
iconData: iconData,
|
iconData: iconData,
|
||||||
contextualIdentity: contextualIdentity,
|
contextualIdentity: contextualIdentity,
|
||||||
@@ -112,6 +124,10 @@ class ContainerMetadata with FastEquatable {
|
|||||||
bypassGlobalProxy: bypassGlobalProxy ?? false,
|
bypassGlobalProxy: bypassGlobalProxy ?? false,
|
||||||
useCustomColor: useCustomColor ?? false,
|
useCustomColor: useCustomColor ?? false,
|
||||||
assignedSites: assignedSites,
|
assignedSites: assignedSites,
|
||||||
|
// Strict mode needs a contextId (the extension keys on cookieStoreId);
|
||||||
|
// normalize away the invalid combination on read, and writers re-apply
|
||||||
|
// it via [sanitized].
|
||||||
|
strictMode: (strictMode ?? false) && contextualIdentity != null,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Enforce the [excludeFromHistory] invariant before persistence: it can only
|
/// Enforce the [excludeFromHistory] invariant before persistence: it can only
|
||||||
@@ -120,10 +136,16 @@ class ContainerMetadata with FastEquatable {
|
|||||||
/// The primary constructor can't normalize (copy_with_extension_gen requires
|
/// The primary constructor can't normalize (copy_with_extension_gen requires
|
||||||
/// params to map 1:1 to fields), so writers route through this.
|
/// params to map 1:1 to fields), so writers route through this.
|
||||||
ContainerMetadata sanitized() {
|
ContainerMetadata sanitized() {
|
||||||
if (excludeFromHistory && contextualIdentity == null) {
|
var result = this;
|
||||||
return copyWith(excludeFromHistory: false);
|
if (result.excludeFromHistory && result.contextualIdentity == null) {
|
||||||
|
result = result.copyWith(excludeFromHistory: false);
|
||||||
}
|
}
|
||||||
return this;
|
// Strict mode is meaningless without a contextId: the extension keys
|
||||||
|
// strictness on the tab's cookieStoreId.
|
||||||
|
if (result.strictMode && result.contextualIdentity == null) {
|
||||||
|
result = result.copyWith(strictMode: false);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get usesTorProxy => proxyConnectionId is TorProxyConnectionId;
|
bool get usesTorProxy => proxyConnectionId is TorProxyConnectionId;
|
||||||
@@ -144,6 +166,7 @@ class ContainerMetadata with FastEquatable {
|
|||||||
bypassGlobalProxy,
|
bypassGlobalProxy,
|
||||||
useCustomColor,
|
useCustomColor,
|
||||||
assignedSites,
|
assignedSites,
|
||||||
|
strictMode,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ abstract class _$ContainerMetadataCWProxy {
|
|||||||
|
|
||||||
ContainerMetadata assignedSites(List<Uri>? assignedSites);
|
ContainerMetadata assignedSites(List<Uri>? assignedSites);
|
||||||
|
|
||||||
|
ContainerMetadata strictMode(bool strictMode);
|
||||||
|
|
||||||
/// Creates a new instance with the provided field values.
|
/// Creates a new instance with the provided field values.
|
||||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ContainerMetadata(...).copyWith.fieldName(value)`.
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ContainerMetadata(...).copyWith.fieldName(value)`.
|
||||||
///
|
///
|
||||||
@@ -42,6 +44,7 @@ abstract class _$ContainerMetadataCWProxy {
|
|||||||
bool bypassGlobalProxy,
|
bool bypassGlobalProxy,
|
||||||
bool useCustomColor,
|
bool useCustomColor,
|
||||||
List<Uri>? assignedSites,
|
List<Uri>? assignedSites,
|
||||||
|
bool strictMode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +90,9 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
|||||||
ContainerMetadata assignedSites(List<Uri>? assignedSites) =>
|
ContainerMetadata assignedSites(List<Uri>? assignedSites) =>
|
||||||
call(assignedSites: assignedSites);
|
call(assignedSites: assignedSites);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ContainerMetadata strictMode(bool strictMode) => call(strictMode: strictMode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// Creates a new instance with the provided field values.
|
/// Creates a new instance with the provided field values.
|
||||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ContainerMetadata(...).copyWith.fieldName(value)`.
|
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ContainerMetadata(...).copyWith.fieldName(value)`.
|
||||||
@@ -105,6 +111,7 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
|||||||
Object? bypassGlobalProxy = const $CopyWithPlaceholder(),
|
Object? bypassGlobalProxy = const $CopyWithPlaceholder(),
|
||||||
Object? useCustomColor = const $CopyWithPlaceholder(),
|
Object? useCustomColor = const $CopyWithPlaceholder(),
|
||||||
Object? assignedSites = const $CopyWithPlaceholder(),
|
Object? assignedSites = const $CopyWithPlaceholder(),
|
||||||
|
Object? strictMode = const $CopyWithPlaceholder(),
|
||||||
}) {
|
}) {
|
||||||
return ContainerMetadata(
|
return ContainerMetadata(
|
||||||
iconData: iconData == const $CopyWithPlaceholder()
|
iconData: iconData == const $CopyWithPlaceholder()
|
||||||
@@ -153,6 +160,11 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
|||||||
? _value.assignedSites
|
? _value.assignedSites
|
||||||
// ignore: cast_nullable_to_non_nullable
|
// ignore: cast_nullable_to_non_nullable
|
||||||
: assignedSites as List<Uri>?,
|
: assignedSites as List<Uri>?,
|
||||||
|
strictMode:
|
||||||
|
strictMode == const $CopyWithPlaceholder() || strictMode == null
|
||||||
|
? _value.strictMode
|
||||||
|
// ignore: cast_nullable_to_non_nullable
|
||||||
|
: strictMode as bool,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,6 +307,7 @@ ContainerMetadata _$ContainerMetadataFromJson(Map<String, dynamic> json) =>
|
|||||||
assignedSites: (json['assignedSites'] as List<dynamic>?)
|
assignedSites: (json['assignedSites'] as List<dynamic>?)
|
||||||
?.map((e) => Uri.parse(e as String))
|
?.map((e) => Uri.parse(e as String))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
strictMode: json['strictMode'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$ContainerMetadataToJson(
|
Map<String, dynamic> _$ContainerMetadataToJson(
|
||||||
@@ -312,6 +325,7 @@ Map<String, dynamic> _$ContainerMetadataToJson(
|
|||||||
'bypassGlobalProxy': instance.bypassGlobalProxy,
|
'bypassGlobalProxy': instance.bypassGlobalProxy,
|
||||||
'useCustomColor': instance.useCustomColor,
|
'useCustomColor': instance.useCustomColor,
|
||||||
'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(),
|
'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(),
|
||||||
|
'strictMode': instance.strictMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
Value? _$JsonConverterFromJson<Json, Value>(
|
Value? _$JsonConverterFromJson<Json, Value>(
|
||||||
|
|||||||
@@ -190,6 +190,33 @@ Stream<List<SiteAssignment>> watchAllAssignedSites(Ref ref) {
|
|||||||
return db.containerDao.allAssignedSites().watch();
|
return db.containerDao.allAssignedSites().watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strict-mode enforcement map: each Gecko cookie-store context that must be
|
||||||
|
/// enforced (a strict container's base context, plus the isolation contexts of
|
||||||
|
/// its isolated tabs) mapped to the base contextualIdentities its site
|
||||||
|
/// assignments are keyed on. Replicated to the container-proxy extension by
|
||||||
|
/// ProxySettingsReplication.
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Stream<Map<String, List<String>>> watchStrictContextAssignments(Ref ref) {
|
||||||
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
|
return db.containerDao.strictContextAssignments().watch().map((rows) {
|
||||||
|
final assignments = <String, Set<String>>{};
|
||||||
|
for (final row in rows) {
|
||||||
|
final contextId = row.contextId;
|
||||||
|
final assignmentContextId = row.assignmentContextId;
|
||||||
|
if (contextId == null || assignmentContextId == null) continue;
|
||||||
|
|
||||||
|
assignments
|
||||||
|
.putIfAbsent(contextId, () => <String>{})
|
||||||
|
.add(assignmentContextId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
for (final entry in assignments.entries)
|
||||||
|
entry.key: entry.value.toList()..sort(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Watches distinct (isolationContextId, containerId) pairs for isolated tabs
|
/// Watches distinct (isolationContextId, containerId) pairs for isolated tabs
|
||||||
/// assigned to containers. Used by ProxySettingsReplication to manage proxy
|
/// assigned to containers. Used by ProxySettingsReplication to manage proxy
|
||||||
/// aliases for isolated contexts.
|
/// aliases for isolated contexts.
|
||||||
|
|||||||
@@ -1146,6 +1146,66 @@ final class WatchAllAssignedSitesProvider
|
|||||||
String _$watchAllAssignedSitesHash() =>
|
String _$watchAllAssignedSitesHash() =>
|
||||||
r'5f658b5733ee20192eb86d3aeb79aa9678dafba8';
|
r'5f658b5733ee20192eb86d3aeb79aa9678dafba8';
|
||||||
|
|
||||||
|
/// Strict-mode enforcement map: each Gecko cookie-store context that must be
|
||||||
|
/// enforced (a strict container's base context, plus the isolation contexts of
|
||||||
|
/// its isolated tabs) mapped to the base contextualIdentities its site
|
||||||
|
/// assignments are keyed on. Replicated to the container-proxy extension by
|
||||||
|
/// ProxySettingsReplication.
|
||||||
|
|
||||||
|
@ProviderFor(watchStrictContextAssignments)
|
||||||
|
final watchStrictContextAssignmentsProvider =
|
||||||
|
WatchStrictContextAssignmentsProvider._();
|
||||||
|
|
||||||
|
/// Strict-mode enforcement map: each Gecko cookie-store context that must be
|
||||||
|
/// enforced (a strict container's base context, plus the isolation contexts of
|
||||||
|
/// its isolated tabs) mapped to the base contextualIdentities its site
|
||||||
|
/// assignments are keyed on. Replicated to the container-proxy extension by
|
||||||
|
/// ProxySettingsReplication.
|
||||||
|
|
||||||
|
final class WatchStrictContextAssignmentsProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<Map<String, List<String>>>,
|
||||||
|
Map<String, List<String>>,
|
||||||
|
Stream<Map<String, List<String>>>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<Map<String, List<String>>>,
|
||||||
|
$StreamProvider<Map<String, List<String>>> {
|
||||||
|
/// Strict-mode enforcement map: each Gecko cookie-store context that must be
|
||||||
|
/// enforced (a strict container's base context, plus the isolation contexts of
|
||||||
|
/// its isolated tabs) mapped to the base contextualIdentities its site
|
||||||
|
/// assignments are keyed on. Replicated to the container-proxy extension by
|
||||||
|
/// ProxySettingsReplication.
|
||||||
|
WatchStrictContextAssignmentsProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'watchStrictContextAssignmentsProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$watchStrictContextAssignmentsHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$StreamProviderElement<Map<String, List<String>>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $StreamProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Map<String, List<String>>> create(Ref ref) {
|
||||||
|
return watchStrictContextAssignments(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$watchStrictContextAssignmentsHash() =>
|
||||||
|
r'01a92529a90baf955c96c35180a50ec21ff913a8';
|
||||||
|
|
||||||
/// Watches distinct (isolationContextId, containerId) pairs for isolated tabs
|
/// Watches distinct (isolationContextId, containerId) pairs for isolated tabs
|
||||||
/// assigned to containers. Used by ProxySettingsReplication to manage proxy
|
/// assigned to containers. Used by ProxySettingsReplication to manage proxy
|
||||||
/// aliases for isolated contexts.
|
/// aliases for isolated contexts.
|
||||||
|
|||||||
+52
-24
@@ -108,6 +108,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
initialContainer.metadata.bypassGlobalProxy,
|
initialContainer.metadata.bypassGlobalProxy,
|
||||||
);
|
);
|
||||||
final assignedSites = useState(initialContainer.metadata.assignedSites);
|
final assignedSites = useState(initialContainer.metadata.assignedSites);
|
||||||
|
final strictMode = useState(initialContainer.metadata.strictMode);
|
||||||
final isPinned = useState(initialContainer.isPinned);
|
final isPinned = useState(initialContainer.isPinned);
|
||||||
|
|
||||||
final textController = useTextEditingController(
|
final textController = useTextEditingController(
|
||||||
@@ -141,6 +142,10 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
bypassGlobalProxy.value,
|
bypassGlobalProxy.value,
|
||||||
useCustomColor: useCustomColor.value,
|
useCustomColor: useCustomColor.value,
|
||||||
assignedSites: assignedSites.value,
|
assignedSites: assignedSites.value,
|
||||||
|
// Strict mode requires a Gecko contextId (the extension keys
|
||||||
|
// strictness on the tab's cookieStoreId). sanitized() enforces the
|
||||||
|
// same invariant defensively on write.
|
||||||
|
strictMode: strictMode.value && contextualIdentity.value != null,
|
||||||
).sanitized(),
|
).sanitized(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -593,31 +598,54 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
color: colorScheme.surfaceContainer,
|
color: colorScheme.surfaceContainer,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
child: ListTile(
|
child: Column(
|
||||||
leading: const Icon(Icons.web),
|
children: [
|
||||||
title: const Text('Assigned Sites'),
|
ListTile(
|
||||||
subtitle: assignedSiteCount > 0
|
leading: const Icon(Icons.web),
|
||||||
? Text(
|
title: const Text('Assigned Sites'),
|
||||||
'$assignedSiteCount ${assignedSiteCount == 1 ? 'rule' : 'rules'} configured',
|
subtitle: assignedSiteCount > 0
|
||||||
)
|
? Text(
|
||||||
: const Text(
|
'$assignedSiteCount ${assignedSiteCount == 1 ? 'rule' : 'rules'} configured',
|
||||||
'Route matching origins into this container',
|
)
|
||||||
),
|
: const Text(
|
||||||
trailing: const Icon(Icons.chevron_right),
|
'Route matching origins into this container',
|
||||||
onTap: () async {
|
),
|
||||||
final result = await showDialog<Set<Uri>>(
|
trailing: const Icon(Icons.chevron_right),
|
||||||
context: context,
|
onTap: () async {
|
||||||
builder: (context) => ContainerSitesScreen(
|
final result = await showDialog<Set<Uri>>(
|
||||||
initialSites: assignedSites.value?.toSet() ?? {},
|
context: context,
|
||||||
),
|
builder: (context) => ContainerSitesScreen(
|
||||||
);
|
initialSites:
|
||||||
|
assignedSites.value?.toSet() ?? {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (result == null || result.isEmpty) {
|
if (result == null || result.isEmpty) {
|
||||||
assignedSites.value = null;
|
assignedSites.value = null;
|
||||||
} else {
|
} else {
|
||||||
assignedSites.value = result.toList();
|
assignedSites.value = result.toList();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
const Divider(height: 1, indent: 56),
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
value:
|
||||||
|
contextualIdentity.value != null &&
|
||||||
|
strictMode.value,
|
||||||
|
title: const Text('Strict Mode'),
|
||||||
|
subtitle: Text(
|
||||||
|
contextualIdentity.value != null
|
||||||
|
? 'Only allow assigned sites to load; block everything else'
|
||||||
|
: 'Requires cookie isolation to be enabled',
|
||||||
|
),
|
||||||
|
secondary: const Icon(MdiIcons.shieldLockOutline),
|
||||||
|
onChanged: (contextualIdentity.value != null)
|
||||||
|
? (value) {
|
||||||
|
strictMode.value = value;
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -119,6 +119,10 @@ class ContainerProxyRepository extends _$ContainerProxyRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setStrictContexts(Map<String, List<String>> contexts) {
|
||||||
|
return _runLocked(body: () => _service.setStrictContexts(contexts));
|
||||||
|
}
|
||||||
|
|
||||||
/// Serialises [body] behind the service lock after the underlying Gecko
|
/// Serialises [body] behind the service lock after the underlying Gecko
|
||||||
/// proxy service reports healthy. Replaces the per-method boilerplate that
|
/// proxy service reports healthy. Replaces the per-method boilerplate that
|
||||||
/// each public mutator used to repeat.
|
/// each public mutator used to repeat.
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ final class ContainerProxyRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$containerProxyRepositoryHash() =>
|
String _$containerProxyRepositoryHash() =>
|
||||||
r'9201bb0cdda570d38adc5fc16d1c7719f3d719a2';
|
r'9d9190edcb06eb1335318172183e354a66626f57';
|
||||||
|
|
||||||
abstract class _$ContainerProxyRepository extends $Notifier<void> {
|
abstract class _$ContainerProxyRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -103,7 +103,9 @@ class MainApp extends HookConsumerWidget {
|
|||||||
child: _SyncEventListener(
|
child: _SyncEventListener(
|
||||||
child: _SandboxCaptureErrorListener(
|
child: _SandboxCaptureErrorListener(
|
||||||
child: _DownloadStoppedListener(
|
child: _DownloadStoppedListener(
|
||||||
child: child ?? const SizedBox.shrink(),
|
child: _StrictContainerBlockListener(
|
||||||
|
child: child ?? const SizedBox.shrink(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -206,6 +208,42 @@ class _DownloadStoppedListener extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Surfaces a snackbar when the container-proxy extension cancels a navigation
|
||||||
|
/// because the active tab's container is in strict mode (only assigned sites
|
||||||
|
/// may load). Strict blocks carry no destination container — the load was
|
||||||
|
/// already cancelled natively — so this listener only notifies the user.
|
||||||
|
class _StrictContainerBlockListener extends HookConsumerWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _StrictContainerBlockListener({required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final siteAssignmentEvents = ref.watch(
|
||||||
|
eventServiceProvider.select((service) => service.siteAssignementEvent),
|
||||||
|
);
|
||||||
|
|
||||||
|
useOnStreamChange(
|
||||||
|
siteAssignmentEvents,
|
||||||
|
onData: (event) {
|
||||||
|
if (!event.strict) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final host = Uri.tryParse(event.url)?.host;
|
||||||
|
ui_helper.showInfoMessage(
|
||||||
|
context,
|
||||||
|
host != null && host.isNotEmpty
|
||||||
|
? '$host is not assigned to this container'
|
||||||
|
: 'This site is not assigned to this container',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MediaQueryData applyAppMediaQueryOverrides({
|
MediaQueryData applyAppMediaQueryOverrides({
|
||||||
required MediaQueryData mediaQuery,
|
required MediaQueryData mediaQuery,
|
||||||
required double uiScaleFactor,
|
required double uiScaleFactor,
|
||||||
|
|||||||
+4
@@ -72,6 +72,10 @@ class GeckoContainerProxyApiImpl : GeckoContainerProxyApi {
|
|||||||
ContainerProxyFeature.scheduleRequest("setSiteAssignments", JSONObject(assignments))
|
ContainerProxyFeature.scheduleRequest("setSiteAssignments", JSONObject(assignments))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setStrictContexts(contexts: Map<String, List<String>>) {
|
||||||
|
ContainerProxyFeature.scheduleRequest("setStrictContexts", JSONObject(contexts))
|
||||||
|
}
|
||||||
|
|
||||||
override fun healthcheck(callback: (Result<Boolean>) -> Unit) {
|
override fun healthcheck(callback: (Result<Boolean>) -> Unit) {
|
||||||
ContainerProxyFeature.scheduleRequestWithResponse("healthcheck", Unit, object :
|
ContainerProxyFeature.scheduleRequestWithResponse("healthcheck", Unit, object :
|
||||||
ResultConsumer<JSONObject> {
|
ResultConsumer<JSONObject> {
|
||||||
|
|||||||
+2
-1
@@ -122,7 +122,8 @@ object ContainerProxyFeature {
|
|||||||
tabId = components.core.store.state.selectedTabId,
|
tabId = components.core.store.state.selectedTabId,
|
||||||
originUrl = details.tryGetString("originUrl"),
|
originUrl = details.tryGetString("originUrl"),
|
||||||
url = details.getString("url"),
|
url = details.getString("url"),
|
||||||
blocked = details.getBoolean("blocked")
|
blocked = details.getBoolean("blocked"),
|
||||||
|
strict = details.optBoolean("strict", false)
|
||||||
)
|
)
|
||||||
) { _ -> }
|
) { _ -> }
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-3
@@ -4658,7 +4658,14 @@ data class ContainerSiteAssignment (
|
|||||||
val tabId: String? = null,
|
val tabId: String? = null,
|
||||||
val originUrl: String? = null,
|
val originUrl: String? = null,
|
||||||
val url: String,
|
val url: String,
|
||||||
val blocked: Boolean
|
val blocked: Boolean,
|
||||||
|
/**
|
||||||
|
* True when the navigation was cancelled because the tab's container is in
|
||||||
|
* strict mode and the target origin is not assigned to it (as opposed to an
|
||||||
|
* ordinary re-open-in-assigned-container block). Strict blocks have no
|
||||||
|
* destination container; the app just surfaces a message.
|
||||||
|
*/
|
||||||
|
val strict: Boolean
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
companion object {
|
companion object {
|
||||||
@@ -4668,7 +4675,8 @@ data class ContainerSiteAssignment (
|
|||||||
val originUrl = pigeonVar_list[2] as String?
|
val originUrl = pigeonVar_list[2] as String?
|
||||||
val url = pigeonVar_list[3] as String
|
val url = pigeonVar_list[3] as String
|
||||||
val blocked = pigeonVar_list[4] as Boolean
|
val blocked = pigeonVar_list[4] as Boolean
|
||||||
return ContainerSiteAssignment(requestId, tabId, originUrl, url, blocked)
|
val strict = pigeonVar_list[5] as Boolean
|
||||||
|
return ContainerSiteAssignment(requestId, tabId, originUrl, url, blocked, strict)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
@@ -4678,6 +4686,7 @@ data class ContainerSiteAssignment (
|
|||||||
originUrl,
|
originUrl,
|
||||||
url,
|
url,
|
||||||
blocked,
|
blocked,
|
||||||
|
strict,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@@ -4688,7 +4697,7 @@ data class ContainerSiteAssignment (
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
val other = other as ContainerSiteAssignment
|
val other = other as ContainerSiteAssignment
|
||||||
return GeckoPigeonUtils.deepEquals(this.requestId, other.requestId) && GeckoPigeonUtils.deepEquals(this.tabId, other.tabId) && GeckoPigeonUtils.deepEquals(this.originUrl, other.originUrl) && GeckoPigeonUtils.deepEquals(this.url, other.url) && GeckoPigeonUtils.deepEquals(this.blocked, other.blocked)
|
return GeckoPigeonUtils.deepEquals(this.requestId, other.requestId) && GeckoPigeonUtils.deepEquals(this.tabId, other.tabId) && GeckoPigeonUtils.deepEquals(this.originUrl, other.originUrl) && GeckoPigeonUtils.deepEquals(this.url, other.url) && GeckoPigeonUtils.deepEquals(this.blocked, other.blocked) && GeckoPigeonUtils.deepEquals(this.strict, other.strict)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -4698,6 +4707,7 @@ data class ContainerSiteAssignment (
|
|||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.originUrl)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.originUrl)
|
||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.url)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.url)
|
||||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.blocked)
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.blocked)
|
||||||
|
result = 31 * result + GeckoPigeonUtils.deepHash(this.strict)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8622,6 +8632,16 @@ interface GeckoContainerProxyApi {
|
|||||||
fun clearContainerProxy(contextId: String)
|
fun clearContainerProxy(contextId: String)
|
||||||
fun removeContainerProxyRelation(contextId: String, proxyId: String)
|
fun removeContainerProxyRelation(contextId: String, proxyId: String)
|
||||||
fun setSiteAssignments(assignments: Map<String, String>)
|
fun setSiteAssignments(assignments: Map<String, String>)
|
||||||
|
/**
|
||||||
|
* Strict-mode enforcement map. Keys are Gecko cookie-store contexts to
|
||||||
|
* enforce (a strict container's base context plus its isolated tabs'
|
||||||
|
* isolation contexts); each value contains the container base contexts that
|
||||||
|
* site assignments are keyed on. Tabs in these contexts may only load
|
||||||
|
* origins assigned to one of the mapped base contexts (exact match, no proxy
|
||||||
|
* equivalence); any other top-level navigation is cancelled and reported
|
||||||
|
* back with `strict = true`.
|
||||||
|
*/
|
||||||
|
fun setStrictContexts(contexts: Map<String, List<String>>)
|
||||||
fun healthcheck(callback: (Result<Boolean>) -> Unit)
|
fun healthcheck(callback: (Result<Boolean>) -> Unit)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -8816,6 +8836,24 @@ interface GeckoContainerProxyApi {
|
|||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setStrictContexts$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val contextsArg = args[0] as Map<String, List<String>>
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.setStrictContexts(contextsArg)
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
GeckoPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
|
|||||||
+57
-13
@@ -149,19 +149,36 @@ export default class BackgroundMain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = URL.parse(options.url);
|
const url = URL.parse(options.url);
|
||||||
if (url !== null && this.store.isSiteOriginAssigned(url)) {
|
if (url === null) {
|
||||||
let cookieStoreId: string
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
if (tab.cookieStoreId?.startsWith(containerIdentifier) === true) {
|
let cookieStoreId: string
|
||||||
cookieStoreId = tab.cookieStoreId.substring(containerIdentifier.length)
|
|
||||||
} else if (tab.cookieStoreId === privateIdentifier) {
|
|
||||||
// Handle private tabs - use 'private' as identifier
|
|
||||||
cookieStoreId = 'private'
|
|
||||||
} else {
|
|
||||||
cookieStoreId = 'general'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cookieStoreId == 'private' || this.store.isSiteOriginInSameContext(url, cookieStoreId)) {
|
if (tab.cookieStoreId?.startsWith(containerIdentifier) === true) {
|
||||||
|
cookieStoreId = tab.cookieStoreId.substring(containerIdentifier.length)
|
||||||
|
} else if (tab.cookieStoreId === privateIdentifier) {
|
||||||
|
// Handle private tabs - use 'private' as identifier
|
||||||
|
cookieStoreId = 'private'
|
||||||
|
} else {
|
||||||
|
cookieStoreId = 'general'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private tabs are never strict. For other tabs, strict enforcement is
|
||||||
|
// keyed on the tab's cookie-store context (base container context, or an
|
||||||
|
// isolation context of a strict container's isolated tab).
|
||||||
|
const isStrict = cookieStoreId !== 'private' &&
|
||||||
|
this.store.isContextStrict(cookieStoreId)
|
||||||
|
|
||||||
|
if (this.store.isSiteOriginAssigned(url)) {
|
||||||
|
// In a strict context the site must be assigned to *this* container's own
|
||||||
|
// base context (exact match, no proxy equivalence). Non-strict contexts
|
||||||
|
// keep the looser proxy/direct-equivalence matching used for routing.
|
||||||
|
const allowed = isStrict
|
||||||
|
? this.store.isSiteOriginStrictlyAllowed(url, cookieStoreId)
|
||||||
|
: (cookieStoreId == 'private' || this.store.isSiteOriginInSameContext(url, cookieStoreId))
|
||||||
|
|
||||||
|
if (allowed) {
|
||||||
if (tab.highlighted) {
|
if (tab.highlighted) {
|
||||||
port.postMessage({
|
port.postMessage({
|
||||||
"type": "assignedSiteRequested",
|
"type": "assignedSiteRequested",
|
||||||
@@ -171,7 +188,8 @@ export default class BackgroundMain {
|
|||||||
"originUrl": options.originUrl,
|
"originUrl": options.originUrl,
|
||||||
"url": options.url,
|
"url": options.url,
|
||||||
"tabId": options.tabId,
|
"tabId": options.tabId,
|
||||||
"blocked": false
|
"blocked": false,
|
||||||
|
"strict": false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -180,6 +198,9 @@ export default class BackgroundMain {
|
|||||||
// even if the tab is not highlighted.
|
// even if the tab is not highlighted.
|
||||||
return {};
|
return {};
|
||||||
} else {
|
} else {
|
||||||
|
// Assigned to a different container: cancel here and let the app re-open
|
||||||
|
// it in its assigned container. This applies to strict contexts too —
|
||||||
|
// the site still doesn't load here, it just routes to where it belongs.
|
||||||
//Only send events when tab is selected
|
//Only send events when tab is selected
|
||||||
if (tab.highlighted) {
|
if (tab.highlighted) {
|
||||||
port.postMessage({
|
port.postMessage({
|
||||||
@@ -190,7 +211,8 @@ export default class BackgroundMain {
|
|||||||
"originUrl": options.originUrl,
|
"originUrl": options.originUrl,
|
||||||
"url": options.url,
|
"url": options.url,
|
||||||
"tabId": options.tabId,
|
"tabId": options.tabId,
|
||||||
"blocked": true
|
"blocked": true,
|
||||||
|
"strict": false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -199,6 +221,28 @@ export default class BackgroundMain {
|
|||||||
cancel: true,
|
cancel: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (isStrict) {
|
||||||
|
// Strict mode: this container may only load origins assigned to it. The
|
||||||
|
// origin is not assigned to any container, so cancel the navigation and
|
||||||
|
// report it as a strict block (no destination container).
|
||||||
|
if (tab.highlighted) {
|
||||||
|
port.postMessage({
|
||||||
|
"type": "assignedSiteRequested",
|
||||||
|
"id": options.requestId,
|
||||||
|
"status": "success",
|
||||||
|
"result": {
|
||||||
|
"originUrl": options.originUrl,
|
||||||
|
"url": options.url,
|
||||||
|
"tabId": options.tabId,
|
||||||
|
"blocked": true,
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cancel: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
+6
-1
@@ -18,7 +18,8 @@ interface Message {
|
|||||||
'clearContainerProxy' |
|
'clearContainerProxy' |
|
||||||
'removeContainerProxyRelation' |
|
'removeContainerProxyRelation' |
|
||||||
'healthcheck' |
|
'healthcheck' |
|
||||||
'setSiteAssignments';
|
'setSiteAssignments' |
|
||||||
|
'setStrictContexts';
|
||||||
args: any;
|
args: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +93,10 @@ port.onMessage.addListener((raw: unknown): void => {
|
|||||||
console.log('set site assignments ' + JSON.stringify(message.args))
|
console.log('set site assignments ' + JSON.stringify(message.args))
|
||||||
store.setSiteAssignments(entries);
|
store.setSiteAssignments(entries);
|
||||||
break
|
break
|
||||||
|
case "setStrictContexts":
|
||||||
|
console.log('set strict contexts ' + JSON.stringify(message.args))
|
||||||
|
store.setStrictContexts(new Map(Object.entries(message.args)) as Map<string, string[]>);
|
||||||
|
break
|
||||||
case "healthcheck":
|
case "healthcheck":
|
||||||
port.postMessage({
|
port.postMessage({
|
||||||
"type": "healthcheck",
|
"type": "healthcheck",
|
||||||
|
|||||||
@@ -89,6 +89,39 @@ export class Store {
|
|||||||
private siteAssignments: Map<string, string> = new Map<string, string>()
|
private siteAssignments: Map<string, string> = new Map<string, string>()
|
||||||
private wildcardAssignments: WildcardAssignment[] = []
|
private wildcardAssignments: WildcardAssignment[] = []
|
||||||
|
|
||||||
|
// Maps an enforced cookie-store context (a strict container's base context,
|
||||||
|
// or an isolation context of one of its isolated tabs) to the base contexts
|
||||||
|
// its site assignments are keyed on.
|
||||||
|
private strictContexts: Map<string, Set<string>> = new Map<string, Set<string>>()
|
||||||
|
|
||||||
|
setStrictContexts(contexts: Map<string, string[]>): void {
|
||||||
|
const next = new Map<string, Set<string>>()
|
||||||
|
for (const [contextId, assignmentContexts] of contexts) {
|
||||||
|
next.set(contextId, new Set(assignmentContexts))
|
||||||
|
}
|
||||||
|
this.strictContexts = next
|
||||||
|
}
|
||||||
|
|
||||||
|
isContextStrict(contextId: string): boolean {
|
||||||
|
return this.strictContexts.has(contextId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when [uri]'s origin is assigned to a base context that [contextId]
|
||||||
|
* enforces. Strict mode requires an exact assignment match (via
|
||||||
|
* [lookupAssignment], which handles exact + wildcard entries) against the
|
||||||
|
* container's base contexts — proxy/direct equivalence is deliberately NOT
|
||||||
|
* consulted, so a site assigned only to a different (even proxy-equivalent)
|
||||||
|
* container does not load here.
|
||||||
|
*/
|
||||||
|
isSiteOriginStrictlyAllowed(uri: URL, contextId: string): boolean {
|
||||||
|
const assignmentContexts = this.strictContexts.get(contextId)
|
||||||
|
const assignedContext = this.lookupAssignment(uri)
|
||||||
|
return assignedContext !== undefined &&
|
||||||
|
assignmentContexts !== undefined &&
|
||||||
|
assignmentContexts.has(assignedContext)
|
||||||
|
}
|
||||||
|
|
||||||
setSiteAssignments(sites: Map<string, unknown>): void {
|
setSiteAssignments(sites: Map<string, unknown>): void {
|
||||||
const exact = new Map<string, string>()
|
const exact = new Map<string, string>()
|
||||||
const wildcard: WildcardAssignment[] = []
|
const wildcard: WildcardAssignment[] = []
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ class GeckoContainerProxyService {
|
|||||||
return _apiInstance.setSiteAssignments(assignments);
|
return _apiInstance.setSiteAssignments(assignments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setStrictContexts(Map<String, List<String>> contexts) {
|
||||||
|
return _apiInstance.setStrictContexts(contexts);
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> healthcheck() async {
|
Future<bool> healthcheck() async {
|
||||||
try {
|
try {
|
||||||
return await _apiInstance.healthcheck().timeout(
|
return await _apiInstance.healthcheck().timeout(
|
||||||
|
|||||||
@@ -4928,6 +4928,7 @@ class ContainerSiteAssignment {
|
|||||||
this.originUrl,
|
this.originUrl,
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.blocked,
|
required this.blocked,
|
||||||
|
required this.strict,
|
||||||
});
|
});
|
||||||
|
|
||||||
String requestId;
|
String requestId;
|
||||||
@@ -4940,6 +4941,12 @@ class ContainerSiteAssignment {
|
|||||||
|
|
||||||
bool blocked;
|
bool blocked;
|
||||||
|
|
||||||
|
/// True when the navigation was cancelled because the tab's container is in
|
||||||
|
/// strict mode and the target origin is not assigned to it (as opposed to an
|
||||||
|
/// ordinary re-open-in-assigned-container block). Strict blocks have no
|
||||||
|
/// destination container; the app just surfaces a message.
|
||||||
|
bool strict;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[
|
return <Object?>[
|
||||||
requestId,
|
requestId,
|
||||||
@@ -4947,6 +4954,7 @@ class ContainerSiteAssignment {
|
|||||||
originUrl,
|
originUrl,
|
||||||
url,
|
url,
|
||||||
blocked,
|
blocked,
|
||||||
|
strict,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4961,6 +4969,7 @@ class ContainerSiteAssignment {
|
|||||||
originUrl: result[2] as String?,
|
originUrl: result[2] as String?,
|
||||||
url: result[3]! as String,
|
url: result[3]! as String,
|
||||||
blocked: result[4]! as bool,
|
blocked: result[4]! as bool,
|
||||||
|
strict: result[5]! as bool,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4973,7 +4982,7 @@ class ContainerSiteAssignment {
|
|||||||
if (identical(this, other)) {
|
if (identical(this, other)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return _deepEquals(requestId, other.requestId) && _deepEquals(tabId, other.tabId) && _deepEquals(originUrl, other.originUrl) && _deepEquals(url, other.url) && _deepEquals(blocked, other.blocked);
|
return _deepEquals(requestId, other.requestId) && _deepEquals(tabId, other.tabId) && _deepEquals(originUrl, other.originUrl) && _deepEquals(url, other.url) && _deepEquals(blocked, other.blocked) && _deepEquals(strict, other.strict);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -8634,6 +8643,31 @@ class GeckoContainerProxyApi {
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strict-mode enforcement map. Keys are Gecko cookie-store contexts to
|
||||||
|
/// enforce (a strict container's base context plus its isolated tabs'
|
||||||
|
/// isolation contexts); each value contains the container base contexts that
|
||||||
|
/// site assignments are keyed on. Tabs in these contexts may only load
|
||||||
|
/// origins assigned to one of the mapped base contexts (exact match, no proxy
|
||||||
|
/// equivalence); any other top-level navigation is cancelled and reported
|
||||||
|
/// back with `strict = true`.
|
||||||
|
Future<void> setStrictContexts(Map<String, List<String>> contexts) async {
|
||||||
|
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.setStrictContexts$pigeonVar_messageChannelSuffix';
|
||||||
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[contexts]);
|
||||||
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
|
_extractReplyValueOrThrow(
|
||||||
|
pigeonVar_replyList,
|
||||||
|
pigeonVar_channelName,
|
||||||
|
isNullValid: true,
|
||||||
|
)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> healthcheck() async {
|
Future<bool> healthcheck() async {
|
||||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$pigeonVar_messageChannelSuffix';
|
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoContainerProxyApi.healthcheck$pigeonVar_messageChannelSuffix';
|
||||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
|||||||
@@ -1927,6 +1927,15 @@ abstract class GeckoContainerProxyApi {
|
|||||||
void removeContainerProxyRelation(String contextId, String proxyId);
|
void removeContainerProxyRelation(String contextId, String proxyId);
|
||||||
void setSiteAssignments(Map<String, String> assignments);
|
void setSiteAssignments(Map<String, String> assignments);
|
||||||
|
|
||||||
|
/// Strict-mode enforcement map. Keys are Gecko cookie-store contexts to
|
||||||
|
/// enforce (a strict container's base context plus its isolated tabs'
|
||||||
|
/// isolation contexts); each value contains the container base contexts that
|
||||||
|
/// site assignments are keyed on. Tabs in these contexts may only load
|
||||||
|
/// origins assigned to one of the mapped base contexts (exact match, no proxy
|
||||||
|
/// equivalence); any other top-level navigation is cancelled and reported
|
||||||
|
/// back with `strict = true`.
|
||||||
|
void setStrictContexts(Map<String, List<String>> contexts);
|
||||||
|
|
||||||
@async
|
@async
|
||||||
bool healthcheck();
|
bool healthcheck();
|
||||||
}
|
}
|
||||||
@@ -1985,12 +1994,19 @@ class ContainerSiteAssignment {
|
|||||||
final String url;
|
final String url;
|
||||||
final bool blocked;
|
final bool blocked;
|
||||||
|
|
||||||
|
/// True when the navigation was cancelled because the tab's container is in
|
||||||
|
/// strict mode and the target origin is not assigned to it (as opposed to an
|
||||||
|
/// ordinary re-open-in-assigned-container block). Strict blocks have no
|
||||||
|
/// destination container; the app just surfaces a message.
|
||||||
|
final bool strict;
|
||||||
|
|
||||||
ContainerSiteAssignment({
|
ContainerSiteAssignment({
|
||||||
required this.requestId,
|
required this.requestId,
|
||||||
required this.tabId,
|
required this.tabId,
|
||||||
required this.originUrl,
|
required this.originUrl,
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.blocked,
|
required this.blocked,
|
||||||
|
required this.strict,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user