156 lines
3.6 KiB
Dart
156 lines
3.6 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:tools/models/device.dart';
|
|
|
|
int _ipToInt(String ip) {
|
|
final parts = ip.split('.').map(int.parse).toList();
|
|
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
|
|
}
|
|
|
|
String _intToIp(int value) {
|
|
return '${(value >> 24) & 0xFF}.'
|
|
'${(value >> 16) & 0xFF}.'
|
|
'${(value >> 8) & 0xFF}.'
|
|
'${value & 0xFF}';
|
|
}
|
|
|
|
Future<bool> _isHostAlive(
|
|
String ip, {
|
|
int port = 80,
|
|
int timeoutMs = 300,
|
|
}) async {
|
|
try {
|
|
final socket = await Socket.connect(
|
|
ip,
|
|
port,
|
|
timeout: Duration(milliseconds: timeoutMs),
|
|
);
|
|
await socket.close();
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<bool> isHostAlive(String ip, {int port = 22, int timeoutMs = 500}) {
|
|
return _isHostAlive(ip, port: port, timeoutMs: timeoutMs);
|
|
}
|
|
|
|
Future<Map<String, bool>> scanSelectedDevicesOnline(
|
|
List<Device> devices, {
|
|
int port = 22,
|
|
int timeoutMs = 500,
|
|
}) async {
|
|
final futures = <Future<MapEntry<String, bool>>>[];
|
|
for (final device in devices) {
|
|
futures.add(
|
|
isHostAlive(
|
|
device.address,
|
|
port: port,
|
|
timeoutMs: timeoutMs,
|
|
).then((isOnline) => MapEntry(device.address, isOnline)),
|
|
);
|
|
}
|
|
|
|
final entries = await Future.wait(futures);
|
|
return Map<String, bool>.fromEntries(entries);
|
|
}
|
|
|
|
String? _extractMac(String source) {
|
|
final lladdrMatch = RegExp(
|
|
r'lladdr\s+([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})',
|
|
caseSensitive: false,
|
|
).firstMatch(source);
|
|
if (lladdrMatch != null) {
|
|
return lladdrMatch.group(1)?.toLowerCase();
|
|
}
|
|
|
|
final genericMatch = RegExp(
|
|
r'([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})',
|
|
caseSensitive: false,
|
|
).firstMatch(source);
|
|
return genericMatch?.group(1)?.toLowerCase();
|
|
}
|
|
|
|
Future<String?> _resolveMacAddress(String ip) async {
|
|
try {
|
|
final ipResult = await Process.run('ip', ['neigh', 'show', ip]);
|
|
final ipOutput = '${ipResult.stdout}\n${ipResult.stderr}';
|
|
final ipMac = _extractMac(ipOutput);
|
|
if (ipMac != null) {
|
|
return ipMac;
|
|
}
|
|
} catch (_) {
|
|
// ignore and fallback
|
|
}
|
|
|
|
try {
|
|
final arpResult = await Process.run('arp', ['-n', ip]);
|
|
final arpOutput = '${arpResult.stdout}\n${arpResult.stderr}';
|
|
return _extractMac(arpOutput);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<Device?> _scanHost(
|
|
String host, {
|
|
required bool Function() isCancelled,
|
|
}) async {
|
|
if (isCancelled()) {
|
|
return null;
|
|
}
|
|
|
|
final isAlive = await _isHostAlive(host, port: 80);
|
|
if (!isAlive || isCancelled()) {
|
|
return null;
|
|
}
|
|
|
|
final mac = await _resolveMacAddress(host);
|
|
|
|
return Device(address: host, mac: mac ?? '');
|
|
}
|
|
|
|
Future<void> scanSubnet(
|
|
String net,
|
|
String mask,
|
|
StreamController<Device> controller, {
|
|
bool Function()? isCancelled,
|
|
int maxConcurrent = 64,
|
|
}) async {
|
|
final shouldCancel = isCancelled ?? () => false;
|
|
final ipInt = _ipToInt(net);
|
|
final maskInt = _ipToInt(mask);
|
|
|
|
final network = ipInt & maskInt;
|
|
final broadcast = network | (~maskInt & 0xFFFFFFFF);
|
|
|
|
if (maxConcurrent < 1) {
|
|
maxConcurrent = 1;
|
|
}
|
|
|
|
for (int start = network + 1; start < broadcast; start += maxConcurrent) {
|
|
if (shouldCancel()) {
|
|
return;
|
|
}
|
|
|
|
final end = (start + maxConcurrent) < broadcast
|
|
? (start + maxConcurrent)
|
|
: broadcast;
|
|
|
|
final futures = <Future<Device?>>[];
|
|
for (int addr = start; addr < end; addr++) {
|
|
futures.add(_scanHost(_intToIp(addr), isCancelled: shouldCancel));
|
|
}
|
|
|
|
final foundDevices = await Future.wait(futures);
|
|
for (final device in foundDevices) {
|
|
if (device == null || shouldCancel() || controller.isClosed) {
|
|
continue;
|
|
}
|
|
controller.add(device);
|
|
}
|
|
}
|
|
}
|