Files
IKAR/lib/core/media/max_media_http.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()),
),
);
}