import 'dart:math'; class H5Line { const H5Line({ required this.label, required this.url, }); final String label; final String url; Uri get uri => Uri.parse(url); } enum Environment { development, staging, production, } class AppConfig { static const String appName = '本地打包'; static const String appLogo = ''; static const String clientConfigDevice = 'h5'; static const String clientConfigSerialNo = 'h5-domain'; static const String _dartDefinedH5LineUrls = String.fromEnvironment('H5_LINE_URLS'); static const String _dartDefinedClientConfigQueryUrl = String.fromEnvironment('CLIENT_CONFIG_QUERY_URL'); // Bootstrap only: used before /client_config/query returns dynamic H5 domains. static const String _bootstrapH5LineUrl = String.fromEnvironment( 'BOOTSTRAP_H5_LINE_URL', defaultValue: 'https://h5-test.imharry.work/'); static const Environment currentEnvironment = Environment.production; // Compatibility for the packaging service's legacy domain rewrite step. static final Map> _environmentHosts = { Environment.production: [ _bootstrapH5LineUrl, ], }; static final Random _wildcardRandom = Random.secure(); static List get h5Lines { final configuredUrls = _dartDefinedH5LineUrls.trim().isEmpty ? _normalizedUniqueUrls(defaultH5LineUrlsForBaseUri(Uri.base)) : _normalizedUniqueUrls(_dartDefinedH5LineUrls.split(',')); final urls = configuredUrls.isEmpty ? _normalizedUniqueUrls(const [_bootstrapH5LineUrl]) : configuredUrls; return [ for (var index = 0; index < urls.length; index += 1) H5Line(label: '线路${index + 1}', url: urls[index]), ]; } static Map get clientConfigQueryPayload => const { 'device': clientConfigDevice, 'serialNo': clientConfigSerialNo, }; static List get clientConfigQueryUris { final dartDefinedUrl = _dartDefinedClientConfigQueryUrl.trim(); if (dartDefinedUrl.isNotEmpty) { return [Uri.parse(dartDefinedUrl)]; } final lines = h5Lines; if (lines.isEmpty) { return const []; } return clientConfigQueryUrisForHomeUri(lines.first.uri); } static List defaultH5LineUrlsForBaseUri(Uri baseUri) { final runtimeHomeUrl = _runtimeHomeUrlFromBaseUri(baseUri); if (runtimeHomeUrl != null) { return [runtimeHomeUrl]; } final environmentHosts = _environmentHosts[currentEnvironment]; if (environmentHosts == null || environmentHosts.isEmpty) { return const [_bootstrapH5LineUrl]; } return environmentHosts; } static List clientConfigQueryUrisForHomeUri(Uri homeUri) { return [ _originUriWithPath(homeUri, '/client_config/query'), _originUriWithPath(homeUri, '/api/user/client_config/query'), ]; } static List h5LinesFromResourceUrl( String resourceUrl, { String Function()? wildcardFactory, }) { final urls = _normalizedUniqueUrls( splitResourceUrlLines(resourceUrl).map( (value) => _replaceWildcardHost( value, wildcardFactory: wildcardFactory, ), ), ); return [ for (var index = 0; index < urls.length; index += 1) H5Line(label: '线路${index + 1}', url: urls[index]), ]; } static List splitResourceUrlLines(String resourceUrl) { if (resourceUrl.trim().isEmpty) { return const []; } final lines = []; final seen = {}; for (final value in resourceUrl.split(RegExp(r'\\r\\n|\\n|\\r|\\t|[\r\n\t]+'))) { final line = value.trim(); if (line.isEmpty || seen.contains(line)) { continue; } lines.add(line); seen.add(line); } return List.unmodifiable(lines); } static bool isLoginPageUrl(String? url) { if (url == null || url.isEmpty) { return false; } final uri = Uri.tryParse(url); if (uri == null) { return false; } return _isLoginPath(uri.path) || _isLoginPath(uri.fragment); } static bool _isLoginPath(String path) { final normalized = path.trim(); if (normalized.isEmpty) { return false; } final pathOnly = normalized.split('?').first.split('#').first; final segments = pathOnly.split('/').where((segment) => segment.isNotEmpty); if (segments.isEmpty) { return false; } return segments.last == 'login'; } static List _normalizedUniqueUrls(Iterable values) { final urls = []; final seen = {}; for (final value in values) { final normalized = _tryNormalizeHomeUrl(value); if (normalized == null || seen.contains(normalized)) { continue; } urls.add(normalized); seen.add(normalized); } return List.unmodifiable(urls); } static String? _tryNormalizeHomeUrl(String value) { try { return _normalizeHomeUrl(value); } catch (_) { return null; } } static String _normalizeHomeUrl(String host) { final value = host.trim(); if (value.isEmpty) { throw const FormatException('Empty H5 line URL'); } final normalized = value.startsWith('http://') || value.startsWith('https://') ? value : 'https://$value'; final uri = Uri.parse(normalized); final path = uri.path.isEmpty ? '/' : uri.path; return uri.replace(path: path).toString(); } static String? _runtimeHomeUrlFromBaseUri(Uri baseUri) { if (_isLocalRuntimeUri(baseUri)) { return null; } return _originUriWithPath(baseUri, '/').toString(); } static Uri _originUriWithPath(Uri uri, String path) { final normalizedPath = path.startsWith('/') ? path : '/$path'; return Uri.parse('${uri.scheme}://${uri.authority}$normalizedPath'); } static bool _isLocalRuntimeUri(Uri uri) { if (uri.scheme != 'http' && uri.scheme != 'https') { return true; } final host = _normalizeHostForCompare(uri.host); if (host.isEmpty) { return true; } if (host == 'localhost' || host.endsWith('.localhost') || host == '::1') { return true; } final ipv4Parts = _tryParseIpv4Address(host); if (ipv4Parts == null) { return false; } return ipv4Parts[0] == 127 || ipv4Parts.every((part) => part == 0); } static String _normalizeHostForCompare(String host) { final normalized = host.trim().toLowerCase(); if (normalized.endsWith('.')) { return normalized.substring(0, normalized.length - 1); } return normalized; } static List? _tryParseIpv4Address(String host) { final parts = host.split('.'); if (parts.length != 4) { return null; } final octets = []; for (final part in parts) { if (part.isEmpty || !RegExp(r'^\d+$').hasMatch(part)) { return null; } final value = int.tryParse(part); if (value == null || value < 0 || value > 255) { return null; } octets.add(value); } return octets; } static String _replaceWildcardHost( String value, { String Function()? wildcardFactory, }) { if (!value.contains('*.')) { return value; } return value.replaceAllMapped( '*.', (_) => '${(wildcardFactory ?? _randomWildcardLabel)()}.', ); } static String _randomWildcardLabel() { const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; return String.fromCharCodes( List.generate( 16, (_) => chars.codeUnitAt(_wildcardRandom.nextInt(chars.length)), ), ); } }