Files
tools/lib/services/scanner.dart
2026-04-07 11:38:37 +03:00

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);
}
}
}