99 lines
3.1 KiB
Dart
99 lines
3.1 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/io_client.dart';
|
|
|
|
/// MAX's media CDN uses the Russian Trusted CA. Keep that additional trust
|
|
/// confined to MAX hosts, including after redirects; never accept bad certs.
|
|
class MaxMediaHttpClient extends http.BaseClient {
|
|
MaxMediaHttpClient({
|
|
http.Client? standard,
|
|
Future<http.Client> Function()? trusted,
|
|
}) : _standard =
|
|
standard ??
|
|
IOClient(
|
|
HttpClient()..connectionTimeout = const Duration(seconds: 30),
|
|
),
|
|
_trustedFactory = trusted ?? _createTrusted;
|
|
|
|
final http.Client _standard;
|
|
final Future<http.Client> Function() _trustedFactory;
|
|
Future<http.Client>? _trusted;
|
|
|
|
static bool usesAdditionalTrust(Uri uri) =>
|
|
uri.scheme == 'https' &&
|
|
(uri.host == 'oneme.ru' || uri.host.endsWith('.oneme.ru'));
|
|
|
|
static Future<http.Client> _createTrusted() async {
|
|
final certificate = await rootBundle.load(
|
|
'assets/certificates/russian_trusted_root_ca.pem',
|
|
);
|
|
final context = SecurityContext(withTrustedRoots: true)
|
|
..setTrustedCertificatesBytes(
|
|
certificate.buffer.asUint8List(
|
|
certificate.offsetInBytes,
|
|
certificate.lengthInBytes,
|
|
),
|
|
);
|
|
return IOClient(
|
|
HttpClient(context: context)
|
|
..connectionTimeout = const Duration(seconds: 30),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
|
if (request.method != 'GET' && request.method != 'HEAD') {
|
|
throw ArgumentError('Media client supports GET and HEAD only');
|
|
}
|
|
var uri = request.url;
|
|
for (var redirects = 0; ; redirects++) {
|
|
final client = usesAdditionalTrust(uri)
|
|
? await (_trusted ??= _trustedFactory())
|
|
: _standard;
|
|
final next = http.Request(request.method, uri)..followRedirects = false;
|
|
if (uri.origin == request.url.origin) {
|
|
next.headers.addAll(request.headers);
|
|
}
|
|
final response = await client.send(next);
|
|
final location = response.headers['location'];
|
|
if (!request.followRedirects ||
|
|
location == null ||
|
|
!const [301, 302, 303, 307, 308].contains(response.statusCode)) {
|
|
return response;
|
|
}
|
|
await response.stream.drain<void>();
|
|
if (redirects >= request.maxRedirects) {
|
|
throw http.ClientException('Too many media redirects');
|
|
}
|
|
final target = uri.resolve(location);
|
|
if ((target.scheme != 'http' && target.scheme != 'https') ||
|
|
(uri.scheme == 'https' && target.scheme != 'https')) {
|
|
throw http.ClientException('Unsafe media redirect');
|
|
}
|
|
uri = target;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void close() {
|
|
_standard.close();
|
|
_trusted?.then<void>(
|
|
(client) => client.close(),
|
|
onError: (Object error, StackTrace stack) {},
|
|
);
|
|
super.close();
|
|
}
|
|
}
|
|
|
|
class MaxMediaCache {
|
|
static final instance = CacheManager(
|
|
Config(
|
|
'qlyraMaxMedia',
|
|
fileService: HttpFileService(httpClient: MaxMediaHttpClient()),
|
|
),
|
|
);
|
|
}
|