container wildcard assignments
This commit is contained in:
@@ -171,10 +171,6 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SingleSelectable<bool> isSiteAssignedToContainer(Uri uri) {
|
|
||||||
return db.definitionsDrift.isSiteAssignedToContainer(uri: uri.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
SingleSelectable<bool> areSitesAvailable(
|
SingleSelectable<bool> areSitesAvailable(
|
||||||
Iterable<Uri> origins,
|
Iterable<Uri> origins,
|
||||||
String ignoredContainerId,
|
String ignoredContainerId,
|
||||||
@@ -185,10 +181,6 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Selectable<String> siteAssignedContainerId(Uri uri) {
|
|
||||||
return db.definitionsDrift.siteAssignedContainerId(uri: uri.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
Selectable<SiteAssignment> allAssignedSites() {
|
Selectable<SiteAssignment> allAssignedSites() {
|
||||||
return db.definitionsDrift.allAssignedSites();
|
return db.definitionsDrift.allAssignedSites();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,14 +297,6 @@ nextTabByOrderKey(:tab_id AS TEXT, :container_id AS TEXT OR NULL, :skip_containe
|
|||||||
FROM ranked_tabs
|
FROM ranked_tabs
|
||||||
WHERE id = :tab_id;
|
WHERE id = :tab_id;
|
||||||
|
|
||||||
isSiteAssignedToContainer:
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM container
|
|
||||||
CROSS JOIN json_each(container.metadata, '$.assignedSites')
|
|
||||||
WHERE json_each.value = :uri
|
|
||||||
) AS existing;
|
|
||||||
|
|
||||||
areSitesAvailable:
|
areSitesAvailable:
|
||||||
SELECT NOT EXISTS (
|
SELECT NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
@@ -317,15 +309,6 @@ areSitesAvailable:
|
|||||||
container.id IS NOT :ignore_container_id
|
container.id IS NOT :ignore_container_id
|
||||||
) AS existing;
|
) AS existing;
|
||||||
|
|
||||||
siteAssignedContainerId:
|
|
||||||
SELECT id
|
|
||||||
FROM container
|
|
||||||
WHERE EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM json_each(container.metadata, '$.assignedSites')
|
|
||||||
WHERE value = :uri
|
|
||||||
);
|
|
||||||
|
|
||||||
allAssignedSites WITH SiteAssignment:
|
allAssignedSites WITH SiteAssignment:
|
||||||
SELECT
|
SELECT
|
||||||
container.id,
|
container.id,
|
||||||
|
|||||||
-16
@@ -2557,14 +2557,6 @@ class DefinitionsDrift extends i9.ModularAccessor {
|
|||||||
).map((i0.QueryRow row) => row.readNullable<String>('next_tab_id'));
|
).map((i0.QueryRow row) => row.readNullable<String>('next_tab_id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
i0.Selectable<bool> isSiteAssignedToContainer({String? uri}) {
|
|
||||||
return customSelect(
|
|
||||||
'SELECT EXISTS (SELECT 1 AS _c0 FROM container CROSS JOIN json_each(container.metadata, \'\$.assignedSites\')WHERE json_each.value = ?1) AS existing',
|
|
||||||
variables: [i0.Variable<String>(uri)],
|
|
||||||
readsFrom: {container},
|
|
||||||
).map((i0.QueryRow row) => row.read<bool>('existing'));
|
|
||||||
}
|
|
||||||
|
|
||||||
i0.Selectable<bool> areSitesAvailable({
|
i0.Selectable<bool> areSitesAvailable({
|
||||||
required String uriList,
|
required String uriList,
|
||||||
required String ignoreContainerId,
|
required String ignoreContainerId,
|
||||||
@@ -2579,14 +2571,6 @@ class DefinitionsDrift extends i9.ModularAccessor {
|
|||||||
).map((i0.QueryRow row) => row.read<bool>('existing'));
|
).map((i0.QueryRow row) => row.read<bool>('existing'));
|
||||||
}
|
}
|
||||||
|
|
||||||
i0.Selectable<String> siteAssignedContainerId({String? uri}) {
|
|
||||||
return customSelect(
|
|
||||||
'SELECT id FROM container WHERE EXISTS (SELECT 1 AS _c0 FROM json_each(container.metadata, \'\$.assignedSites\')WHERE value = ?1)',
|
|
||||||
variables: [i0.Variable<String>(uri)],
|
|
||||||
readsFrom: {container},
|
|
||||||
).map((i0.QueryRow row) => row.read<String>('id'));
|
|
||||||
}
|
|
||||||
|
|
||||||
i0.Selectable<i11.SiteAssignment> allAssignedSites() {
|
i0.Selectable<i11.SiteAssignment> allAssignedSites() {
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'SELECT container.id, COALESCE(container.metadata ->> \'\$.contextualIdentity\', \'general\') AS contextualIdentity, value AS assigned_site FROM container CROSS JOIN json_each(container.metadata, \'\$.assignedSites\')WHERE value IS NOT NULL',
|
'SELECT container.id, COALESCE(container.metadata ->> \'\$.contextualIdentity\', \'general\') AS contextualIdentity, value AS assigned_site FROM container CROSS JOIN json_each(container.metadata, \'\$.assignedSites\')WHERE value IS NOT NULL',
|
||||||
|
|||||||
@@ -34,3 +34,27 @@ class SiteAssignment with FastEquatable {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [id, contextualIdentity, assignedSite];
|
List<Object?> get hashParameters => [id, contextualIdentity, assignedSite];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const String _wildcardPrefix = '*.';
|
||||||
|
|
||||||
|
/// A host starting with `*.` (e.g. `*.example.com`) matches the bare apex
|
||||||
|
/// and any subdomain of it.
|
||||||
|
bool isWildcardSite(Uri assignment) =>
|
||||||
|
assignment.host.startsWith(_wildcardPrefix);
|
||||||
|
|
||||||
|
/// Tests whether [assignment] (a stored site-assignment entry) matches the
|
||||||
|
/// given [request] URL. Exact entries compare origins. Wildcard entries
|
||||||
|
/// (host starting with `*.`) match the apex and any subdomain with the
|
||||||
|
/// same scheme, ignoring port.
|
||||||
|
bool siteAssignmentMatches(Uri assignment, Uri request) {
|
||||||
|
if (assignment.scheme != request.scheme) return false;
|
||||||
|
|
||||||
|
if (isWildcardSite(assignment)) {
|
||||||
|
final suffix = assignment.host.substring(_wildcardPrefix.length);
|
||||||
|
if (suffix.isEmpty) return false;
|
||||||
|
final host = request.host;
|
||||||
|
return host == suffix || host.endsWith('.$suffix');
|
||||||
|
}
|
||||||
|
|
||||||
|
return assignment.origin == request.origin;
|
||||||
|
}
|
||||||
|
|||||||
@@ -201,5 +201,9 @@ Stream<bool> watchIsCurrentSiteAssignedToContainer(Ref ref) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final db = ref.watch(tabDatabaseProvider);
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
return db.containerDao.isSiteAssignedToContainer(currentUri).watchSingle();
|
return db.containerDao.allAssignedSites().watch().map((assignments) {
|
||||||
|
return assignments.any(
|
||||||
|
(a) => siteAssignmentMatches(a.assignedSite, currentUri),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1093,4 +1093,4 @@ final class WatchIsCurrentSiteAssignedToContainerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$watchIsCurrentSiteAssignedToContainerHash() =>
|
String _$watchIsCurrentSiteAssignedToContainerHash() =>
|
||||||
r'c2a82ca9df526c7a703da52302182e497c91e824';
|
r'b4e39fecacc84f53adfd1d6368a70a84db181661';
|
||||||
|
|||||||
+16
-11
@@ -24,6 +24,7 @@ import 'package:nullability/nullability.dart';
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:weblibre/core/uuid.dart';
|
import 'package:weblibre/core/uuid.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/utils/color_palette.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/utils/color_palette.dart';
|
||||||
@@ -158,12 +159,8 @@ class ContainerRepository extends _$ContainerRepository {
|
|||||||
return ContainerData(id: uuid.v7(), color: initialColor);
|
return ContainerData(id: uuid.v7(), color: initialColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> isSiteAssignedToContainer(Uri uri) {
|
Future<bool> isSiteAssignedToContainer(Uri uri) async {
|
||||||
return ref
|
return (await siteAssignedContainerId(uri)) != null;
|
||||||
.read(tabDatabaseProvider)
|
|
||||||
.containerDao
|
|
||||||
.isSiteAssignedToContainer(uri)
|
|
||||||
.getSingle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> areSitesAvailable(
|
Future<bool> areSitesAvailable(
|
||||||
@@ -177,13 +174,21 @@ class ContainerRepository extends _$ContainerRepository {
|
|||||||
.getSingle();
|
.getSingle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> siteAssignedContainerId(Uri uri) {
|
Future<String?> siteAssignedContainerId(Uri uri) async {
|
||||||
return ref
|
final all = await ref
|
||||||
.read(tabDatabaseProvider)
|
.read(tabDatabaseProvider)
|
||||||
.containerDao
|
.containerDao
|
||||||
.siteAssignedContainerId(uri)
|
.allAssignedSites()
|
||||||
.get()
|
.get();
|
||||||
.then((value) => value.firstOrNull);
|
|
||||||
|
String? wildcardMatch;
|
||||||
|
for (final a in all) {
|
||||||
|
if (siteAssignmentMatches(a.assignedSite, uri)) {
|
||||||
|
if (!isWildcardSite(a.assignedSite)) return a.id;
|
||||||
|
wildcardMatch ??= a.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wildcardMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> getContainersToClearOnExit() async {
|
Future<List<String>> getContainersToClearOnExit() async {
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ final class ContainerRepositoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$containerRepositoryHash() =>
|
String _$containerRepositoryHash() =>
|
||||||
r'57ee339087a845e54dc79224e25fa0da19568a68';
|
r'55c3e897f4f42d01dab43114ef00075536b74cf7';
|
||||||
|
|
||||||
abstract class _$ContainerRepository extends $Notifier<void> {
|
abstract class _$ContainerRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+65
-33
@@ -22,11 +22,54 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||||
import 'package:weblibre/utils/form_validators.dart';
|
import 'package:weblibre/utils/form_validators.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
|
final _wildcardHostRegex = RegExp(
|
||||||
|
r'^\*\.([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,63}$',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Parses a user-entered site-assignment value. Accepts:
|
||||||
|
/// - `example.com`, `https://example.com/path` → exact origin entry
|
||||||
|
/// - `*.example.com`, `https://*.example.com` → wildcard entry for all
|
||||||
|
/// subdomains (and the apex) of `example.com`
|
||||||
|
Uri? _parseSiteAssignmentInput(String? input) {
|
||||||
|
if (input == null) return null;
|
||||||
|
final trimmed = input.trim();
|
||||||
|
if (trimmed.isEmpty) return null;
|
||||||
|
|
||||||
|
// Strip scheme for wildcard detection.
|
||||||
|
var rest = trimmed;
|
||||||
|
var scheme = 'https';
|
||||||
|
final schemeMatch = RegExp(r'^(https?):\/\/', caseSensitive: false)
|
||||||
|
.firstMatch(trimmed);
|
||||||
|
if (schemeMatch != null) {
|
||||||
|
scheme = schemeMatch.group(1)!.toLowerCase();
|
||||||
|
rest = trimmed.substring(schemeMatch.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract host portion (up to first path/query/fragment/port separator).
|
||||||
|
final hostEnd = rest.indexOf(RegExp(r'[\/?#:]'));
|
||||||
|
final host = (hostEnd == -1 ? rest : rest.substring(0, hostEnd))
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
if (host.startsWith('*.')) {
|
||||||
|
if (!_wildcardHostRegex.hasMatch(host)) return null;
|
||||||
|
return Uri(scheme: scheme, host: host);
|
||||||
|
}
|
||||||
|
|
||||||
|
final parsed = parseValidatedUrl(
|
||||||
|
trimmed,
|
||||||
|
eagerParsing: true,
|
||||||
|
onlyHttpProtocol: true,
|
||||||
|
);
|
||||||
|
if (parsed == null) return null;
|
||||||
|
return Uri.parse(parsed.origin);
|
||||||
|
}
|
||||||
|
|
||||||
class ContainerSitesScreen extends HookConsumerWidget {
|
class ContainerSitesScreen extends HookConsumerWidget {
|
||||||
final Set<Uri> initialSites;
|
final Set<Uri> initialSites;
|
||||||
|
|
||||||
@@ -66,7 +109,9 @@ class ContainerSitesScreen extends HookConsumerWidget {
|
|||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
label: const Text('Add Site'),
|
label: const Text('Add Site'),
|
||||||
hintText: 'example.com',
|
hintText: 'example.com or *.example.com',
|
||||||
|
helperText:
|
||||||
|
'Use *.example.com to match all subdomains',
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
suffix: TextButton(
|
suffix: TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -80,55 +125,42 @@ class ContainerSitesScreen extends HookConsumerWidget {
|
|||||||
controller: textController,
|
controller: textController,
|
||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
final uriValid = validateUrl(
|
if (value.isEmpty) {
|
||||||
value,
|
return 'URL must be provided';
|
||||||
onlyHttpProtocol: true,
|
|
||||||
eagerParsing: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (uriValid != null) {
|
|
||||||
return uriValid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final parsedUrl = parseValidatedUrl(
|
final entry = _parseSiteAssignmentInput(value);
|
||||||
value,
|
if (entry == null) {
|
||||||
eagerParsing: true,
|
|
||||||
onlyHttpProtocol: true,
|
|
||||||
);
|
|
||||||
if (parsedUrl == null) {
|
|
||||||
return 'Invalid URL';
|
return 'Invalid URL';
|
||||||
}
|
}
|
||||||
|
|
||||||
final origin = Uri.parse(parsedUrl.origin);
|
if (sites.value.contains(entry)) {
|
||||||
|
|
||||||
if (sites.value.contains(origin)) {
|
|
||||||
return 'This site has been already assigned';
|
return 'This site has been already assigned';
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
onSaved: (newValue) async {
|
onSaved: (newValue) async {
|
||||||
final parsedUrl = parseValidatedUrl(
|
final entry = _parseSiteAssignmentInput(newValue);
|
||||||
newValue,
|
if (entry == null) {
|
||||||
eagerParsing: true,
|
|
||||||
onlyHttpProtocol: true,
|
|
||||||
);
|
|
||||||
if (parsedUrl == null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final origin = Uri.parse(parsedUrl.origin);
|
// Exact-origin duplicate detection — wildcard overlaps
|
||||||
|
// with existing entries are the user's intent and are
|
||||||
final isAssigned = await ref
|
// not flagged here.
|
||||||
.read(containerRepositoryProvider.notifier)
|
final isAssigned =
|
||||||
.isSiteAssignedToContainer(origin);
|
!isWildcardSite(entry) &&
|
||||||
|
await ref
|
||||||
|
.read(containerRepositoryProvider.notifier)
|
||||||
|
.isSiteAssignedToContainer(entry);
|
||||||
|
|
||||||
if (!isAssigned) {
|
if (!isAssigned) {
|
||||||
sites.value = {...sites.value, origin};
|
sites.value = {...sites.value, entry};
|
||||||
} else {
|
} else {
|
||||||
final assignedContainerId = await ref
|
final assignedContainerId = await ref
|
||||||
.read(containerRepositoryProvider.notifier)
|
.read(containerRepositoryProvider.notifier)
|
||||||
.siteAssignedContainerId(origin);
|
.siteAssignedContainerId(entry);
|
||||||
final assignedContainer = await assignedContainerId
|
final assignedContainer = await assignedContainerId
|
||||||
.mapNotNull(
|
.mapNotNull(
|
||||||
(id) => ref
|
(id) => ref
|
||||||
@@ -140,8 +172,8 @@ class ContainerSitesScreen extends HookConsumerWidget {
|
|||||||
ui_helper.showErrorMessage(
|
ui_helper.showErrorMessage(
|
||||||
context,
|
context,
|
||||||
(assignedContainer?.name.isNotEmpty ?? false)
|
(assignedContainer?.name.isNotEmpty ?? false)
|
||||||
? '$origin has already been assigned to container "${assignedContainer?.name}"'
|
? '$entry has already been assigned to container "${assignedContainer?.name}"'
|
||||||
: '$origin has already been assigned to another container',
|
: '$entry has already been assigned to another container',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,22 +75,76 @@ function fillInDefaults(proxy: Partial<ProxyDao>): ProxyDao {
|
|||||||
return proxy as ProxyDao
|
return proxy as ProxyDao
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WildcardAssignment {
|
||||||
|
protocol: string
|
||||||
|
hostSuffix: string
|
||||||
|
contextId: string
|
||||||
|
}
|
||||||
|
|
||||||
export class Store {
|
export class Store {
|
||||||
private proxies: ProxyDao[] = []
|
private proxies: ProxyDao[] = []
|
||||||
private relations: { [key: string]: string[] } = {}
|
private relations: { [key: string]: string[] } = {}
|
||||||
|
|
||||||
private siteAssignments: Map<string, string> = new Map<string, string>()
|
private siteAssignments: Map<string, string> = new Map<string, string>()
|
||||||
|
private wildcardAssignments: WildcardAssignment[] = []
|
||||||
|
|
||||||
setSiteAssignments(sites: Map<string, unknown>): void {
|
setSiteAssignments(sites: Map<string, unknown>): void {
|
||||||
this.siteAssignments = new Map(
|
const exact = new Map<string, string>()
|
||||||
Array.from(sites, ([key, value]) => {
|
const wildcard: WildcardAssignment[] = []
|
||||||
return [URL.parse(key)!.origin, value as string]
|
|
||||||
})
|
// `*` is a forbidden host code point in WHATWG URL, so wildcard entries
|
||||||
);
|
// must be detected by string match before any URL parser touches them.
|
||||||
|
const wildcardRe = /^(https?):\/\/\*\.([^/?#]+)$/i
|
||||||
|
|
||||||
|
for (const [key, value] of sites) {
|
||||||
|
const contextId = value as string
|
||||||
|
const wildcardMatch = wildcardRe.exec(key)
|
||||||
|
|
||||||
|
if (wildcardMatch !== null) {
|
||||||
|
const hostSuffix = wildcardMatch[2].toLowerCase()
|
||||||
|
if (hostSuffix.length === 0) continue
|
||||||
|
wildcard.push({
|
||||||
|
protocol: wildcardMatch[1].toLowerCase() + ':',
|
||||||
|
hostSuffix,
|
||||||
|
contextId,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = URL.parse(key)
|
||||||
|
if (parsed === null) continue
|
||||||
|
exact.set(parsed.origin, contextId)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.siteAssignments = exact
|
||||||
|
this.wildcardAssignments = wildcard
|
||||||
|
}
|
||||||
|
|
||||||
|
private matchWildcard(uri: URL): string | undefined {
|
||||||
|
// Longest suffix wins, so e.g. *.sub.example.com beats *.example.com.
|
||||||
|
let bestMatch: WildcardAssignment | undefined
|
||||||
|
for (const entry of this.wildcardAssignments) {
|
||||||
|
if (entry.protocol !== uri.protocol) continue
|
||||||
|
const host = uri.hostname
|
||||||
|
if (host !== entry.hostSuffix && !host.endsWith('.' + entry.hostSuffix)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
bestMatch === undefined ||
|
||||||
|
entry.hostSuffix.length > bestMatch.hostSuffix.length
|
||||||
|
) {
|
||||||
|
bestMatch = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestMatch?.contextId
|
||||||
|
}
|
||||||
|
|
||||||
|
private lookupAssignment(uri: URL): string | undefined {
|
||||||
|
return this.siteAssignments.get(uri.origin) ?? this.matchWildcard(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
isSiteOriginAssigned(uri: URL): boolean {
|
isSiteOriginAssigned(uri: URL): boolean {
|
||||||
return this.siteAssignments.has(uri.origin)
|
return this.lookupAssignment(uri) !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,7 +159,7 @@ export class Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isSiteOriginInSameContext(uri: URL, contextId: string): boolean {
|
isSiteOriginInSameContext(uri: URL, contextId: string): boolean {
|
||||||
const assignedContextId = this.siteAssignments.get(uri.origin);
|
const assignedContextId = this.lookupAssignment(uri);
|
||||||
if (assignedContextId === undefined) return false;
|
if (assignedContextId === undefined) return false;
|
||||||
if (assignedContextId === contextId) return true;
|
if (assignedContextId === contextId) return true;
|
||||||
|
|
||||||
@@ -119,8 +173,8 @@ export class Store {
|
|||||||
// empty relations mean no proxy, and different non-proxied contexts
|
// empty relations mean no proxy, and different non-proxied contexts
|
||||||
// should not be considered equivalent.
|
// should not be considered equivalent.
|
||||||
if (assignedRelation.length > 0 &&
|
if (assignedRelation.length > 0 &&
|
||||||
assignedRelation.length === currentRelation.length &&
|
assignedRelation.length === currentRelation.length &&
|
||||||
assignedRelation.every((id, i) => id === currentRelation[i])) {
|
assignedRelation.every((id, i) => id === currentRelation[i])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
@@ -165,6 +165,54 @@ describe('Store', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('wildcard site assignments', function () {
|
||||||
|
it('matches subdomains of a wildcard entry', () => {
|
||||||
|
store.setSiteAssignments(new Map([['https://*.example.com', 'ctx1']]))
|
||||||
|
|
||||||
|
expect(store.isSiteOriginAssigned(new URL('https://foo.example.com/path'))).to.be.true
|
||||||
|
expect(store.isSiteOriginAssigned(new URL('https://a.b.example.com/'))).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches the apex of a wildcard entry', () => {
|
||||||
|
store.setSiteAssignments(new Map([['https://*.example.com', 'ctx1']]))
|
||||||
|
|
||||||
|
expect(store.isSiteOriginAssigned(new URL('https://example.com/path'))).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not match unrelated domains', () => {
|
||||||
|
store.setSiteAssignments(new Map([['https://*.example.com', 'ctx1']]))
|
||||||
|
|
||||||
|
expect(store.isSiteOriginAssigned(new URL('https://notexample.com/'))).to.be.false
|
||||||
|
expect(store.isSiteOriginAssigned(new URL('https://example.com.evil.test/'))).to.be.false
|
||||||
|
})
|
||||||
|
|
||||||
|
it('respects scheme when matching wildcards', () => {
|
||||||
|
store.setSiteAssignments(new Map([['https://*.example.com', 'ctx1']]))
|
||||||
|
|
||||||
|
expect(store.isSiteOriginAssigned(new URL('http://foo.example.com/'))).to.be.false
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prefers exact match over wildcard', () => {
|
||||||
|
store.setSiteAssignments(new Map([
|
||||||
|
['https://*.example.com', 'wild_ctx'],
|
||||||
|
['https://foo.example.com', 'exact_ctx'],
|
||||||
|
]))
|
||||||
|
|
||||||
|
expect(store.isSiteOriginInSameContext(new URL('https://foo.example.com/'), 'exact_ctx')).to.be.true
|
||||||
|
expect(store.isSiteOriginInSameContext(new URL('https://bar.example.com/'), 'wild_ctx')).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('prefers longer suffix when multiple wildcards match', () => {
|
||||||
|
store.setSiteAssignments(new Map([
|
||||||
|
['https://*.example.com', 'broad_ctx'],
|
||||||
|
['https://*.sub.example.com', 'narrow_ctx'],
|
||||||
|
]))
|
||||||
|
|
||||||
|
expect(store.isSiteOriginInSameContext(new URL('https://x.sub.example.com/'), 'narrow_ctx')).to.be.true
|
||||||
|
expect(store.isSiteOriginInSameContext(new URL('https://x.other.example.com/'), 'broad_ctx')).to.be.true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('isSiteOriginInSameContext', function () {
|
describe('isSiteOriginInSameContext', function () {
|
||||||
it('should allow proxy-equivalent isolated context', async () => {
|
it('should allow proxy-equivalent isolated context', async () => {
|
||||||
store.setSiteAssignments(new Map([['https://example.com/page', 'container_ctx']]))
|
store.setSiteAssignments(new Map([['https://example.com/page', 'container_ctx']]))
|
||||||
|
|||||||
Reference in New Issue
Block a user