The scan omitted six files the config itself cites, so jsdelivr.net, api.github.com and image.tmdb.org were listed as system-only while no scanned source referenced them; changing those hosts would have fallen through to the base config and its user certificate authorities undetected. Also assert the reverse direction, so a domain no scanned source produces fails instead of silently losing its guard.
116 lines
4.4 KiB
Dart
116 lines
4.4 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:xml/xml.dart';
|
|
|
|
const _configPath = 'android/app/src/main/res/xml/network_security_config.xml';
|
|
const _expectedDomains = <String>{
|
|
'plex.tv',
|
|
'plezy.app',
|
|
'trakt.tv',
|
|
'myanimelist.net',
|
|
'anilist.co',
|
|
'simkl.com',
|
|
'simkl.in',
|
|
'jsdelivr.net',
|
|
'api.github.com',
|
|
'image.tmdb.org',
|
|
};
|
|
const _fixedEndpointSourcePaths = <String>[
|
|
'lib/services/plex_auth_service.dart',
|
|
'lib/services/plex_discover_client.dart',
|
|
'lib/services/plex_client/parts/live_tv.dart',
|
|
'lib/services/trakt/trakt_constants.dart',
|
|
'lib/services/trackers/mal/mal_constants.dart',
|
|
'lib/services/trackers/anilist/anilist_constants.dart',
|
|
'lib/services/trackers/simkl/simkl_constants.dart',
|
|
'lib/services/trackers/oauth_proxy_client.dart',
|
|
'lib/services/trackers/anime_lists_mapping_store.dart',
|
|
'lib/services/trackers/fribb_mapping_store.dart',
|
|
'lib/services/catalog/seerr_catalog_source.dart',
|
|
'lib/services/discord_rpc_service.dart',
|
|
'lib/services/update_service.dart',
|
|
'lib/watch_together/services/watch_together_relay_endpoint.dart',
|
|
'lib/main.dart',
|
|
];
|
|
|
|
void main() {
|
|
late XmlDocument config;
|
|
|
|
setUpAll(() {
|
|
config = XmlDocument.parse(File(_configPath).readAsStringSync());
|
|
});
|
|
|
|
test('base config retains user-installed certificate authorities', () {
|
|
final baseConfig = config.rootElement.findElements('base-config').single;
|
|
final certificateSources = baseConfig
|
|
.findAllElements('certificates')
|
|
.map((certificate) => certificate.getAttribute('src'));
|
|
|
|
expect(certificateSources, contains('user'));
|
|
});
|
|
|
|
test('fixed endpoint domains trust only system certificate authorities', () {
|
|
final domainConfigs = config.rootElement.findElements('domain-config').toList();
|
|
|
|
expect(domainConfigs, hasLength(1));
|
|
for (final domainConfig in domainConfigs) {
|
|
final certificateSources = domainConfig
|
|
.findAllElements('certificates')
|
|
.map((certificate) => certificate.getAttribute('src'));
|
|
expect(certificateSources, isNot(contains('user')));
|
|
}
|
|
|
|
final firstPartyConfig = domainConfigs.single;
|
|
final domains = firstPartyConfig.findElements('domain').toList();
|
|
expect(domains.map((domain) => domain.innerText.trim()).toSet(), unorderedEquals(_expectedDomains));
|
|
expect(
|
|
domains.map((domain) => domain.getAttribute('includeSubdomains')),
|
|
everyElement('true'),
|
|
reason: 'Every fixed parent domain must also protect its hard-coded subdomains',
|
|
);
|
|
|
|
final trustAnchors = firstPartyConfig.findElements('trust-anchors').single;
|
|
final certificates = trustAnchors.findElements('certificates').toList();
|
|
expect(certificates, hasLength(1));
|
|
expect(certificates.single.getAttribute('src'), 'system');
|
|
});
|
|
|
|
test('hard-coded HTTPS hosts remain covered by a system-only domain', () {
|
|
final httpsLiteralPattern = RegExp("https://[^'\"\\s]+");
|
|
final discoveredHosts = <String>{};
|
|
|
|
for (final sourcePath in _fixedEndpointSourcePaths) {
|
|
final sourceLines = File(sourcePath).readAsLinesSync();
|
|
for (final line in sourceLines.where((line) => !line.trimLeft().startsWith('//'))) {
|
|
for (final match in httpsLiteralPattern.allMatches(line)) {
|
|
final literal = match.group(0)!;
|
|
final host = Uri.parse(literal).host.toLowerCase();
|
|
discoveredHosts.add(host);
|
|
expect(
|
|
_expectedDomains.any((domain) => host == domain || host.endsWith('.$domain')),
|
|
isTrue,
|
|
reason: '$literal in $sourcePath is not covered by $_configPath',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(discoveredHosts, isNotEmpty, reason: 'The fixed-endpoint source scan must discover HTTPS literals');
|
|
|
|
// The other direction: a listed domain that no scanned source produces means
|
|
// the scan lost sight of the file that owns it, and a host change there would
|
|
// silently fall through to base-config and its user certificate authorities.
|
|
final unobservedDomains = _expectedDomains
|
|
.where((domain) => !discoveredHosts.any((host) => host == domain || host.endsWith('.$domain')))
|
|
.toList();
|
|
expect(
|
|
unobservedDomains,
|
|
isEmpty,
|
|
reason:
|
|
'No scanned source in _fixedEndpointSourcePaths references these domains, so drift in them '
|
|
'cannot be detected. Add the owning file to the scan or drop the domain from $_configPath.',
|
|
);
|
|
});
|
|
}
|