mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 15:02:21 +02:00
82 lines
2.8 KiB
Dart
82 lines
2.8 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
class LocalNetworkPermissionException implements Exception {
|
|
const LocalNetworkPermissionException();
|
|
|
|
@override
|
|
String toString() => 'Local network permission is required';
|
|
}
|
|
|
|
/// Android 17 gates LAN sockets, including SMB and local WebDAV/HTTP servers.
|
|
class LocalNetworkAccess {
|
|
LocalNetworkAccess({
|
|
bool? isAndroid,
|
|
MethodChannel channel = const MethodChannel(
|
|
'com.zarz.spotiflac/local_network',
|
|
),
|
|
Future<List<InternetAddress>> Function(String)? lookup,
|
|
}) : _isAndroid = isAndroid ?? Platform.isAndroid,
|
|
_channel = channel,
|
|
_lookup = lookup ?? InternetAddress.lookup;
|
|
|
|
static final instance = LocalNetworkAccess();
|
|
final bool _isAndroid;
|
|
final MethodChannel _channel;
|
|
final Future<List<InternetAddress>> Function(String) _lookup;
|
|
Future<void>? _pending;
|
|
|
|
Future<void> ensureAccess(Uri target) async {
|
|
if (!_isAndroid) return;
|
|
// Also returns true on older Android releases. Recheck after revocation.
|
|
if (await _channel.invokeMethod<bool>('isGranted') == true) return;
|
|
if (!await _isLocalTarget(target)) return;
|
|
await (_pending ??= _request().whenComplete(() => _pending = null));
|
|
}
|
|
|
|
Future<void> _request() async {
|
|
if (await _channel.invokeMethod<bool>('request') != true) {
|
|
throw const LocalNetworkPermissionException();
|
|
}
|
|
}
|
|
|
|
Future<bool> _isLocalTarget(Uri target) async {
|
|
final host = target.host.toLowerCase().replaceFirst(RegExp(r'\.$'), '');
|
|
final address = InternetAddress.tryParse(host.split('%').first);
|
|
if (address != null) return _isLocalAddress(address);
|
|
if (host == 'localhost' || host.endsWith('.localhost')) return false;
|
|
// mDNS itself needs permission, so do not resolve .local first.
|
|
if (!host.contains('.') ||
|
|
host.endsWith('.local') ||
|
|
host == 'home.arpa' ||
|
|
host.endsWith('.home.arpa')) {
|
|
return true;
|
|
}
|
|
// A public DNS name can still point at a NAS on the local network.
|
|
return (await _lookup(
|
|
host,
|
|
).timeout(const Duration(seconds: 5))).any(_isLocalAddress);
|
|
}
|
|
|
|
static bool _isLocalAddress(InternetAddress address) {
|
|
if (address.isLoopback) return false;
|
|
if (address.isLinkLocal) return true;
|
|
var bytes = address.rawAddress;
|
|
if (bytes.length == 16) {
|
|
if (bytes.take(10).every((byte) => byte == 0) &&
|
|
bytes[10] == 255 &&
|
|
bytes[11] == 255) {
|
|
bytes = bytes.sublist(12);
|
|
} else {
|
|
return bytes[0] & 0xfe == 0xfc;
|
|
}
|
|
}
|
|
return bytes[0] == 10 ||
|
|
(bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) ||
|
|
(bytes[0] == 192 && bytes[1] == 168) ||
|
|
(bytes[0] == 169 && bytes[1] == 254) ||
|
|
(bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127);
|
|
}
|
|
}
|