Line data Source code
1 : import 'dart:async';
2 : import 'dart:isolate';
3 : import 'dart:math';
4 :
5 : import 'package:dart_ping/dart_ping.dart';
6 : import 'package:network_tools/network_tools.dart';
7 : import 'package:network_tools/src/injection.dart';
8 : import 'package:network_tools/src/network_tools_utils.dart';
9 : import 'package:network_tools/src/repository/repository.dart';
10 : import 'package:universal_io/io.dart';
11 :
12 : /// Scans for all hosts in a subnet.
13 : class HostScannerServiceImpl extends HostScannerService {
14 : /// Scans for all hosts in a particular subnet (e.g., 192.168.1.0/24)
15 : /// Set maxHost to higher value if you are not getting results.
16 : /// It won't firstHostId again unless previous scan is completed due to heavy
17 : /// resource consumption.
18 : /// Use hostIds to limit subnet scan to hosts given.
19 : /// [resultsInAddressAscendingOrder] = false will return results faster but not in
20 : /// ascending order and without [progressCallback].
21 1 : @override
22 : Stream<ActiveHost> getAllPingableDevices(
23 : String subnet, {
24 : int firstHostId = HostScannerService.defaultFirstHostId,
25 : int lastHostId = HostScannerService.defaultLastHostId,
26 : List<int> hostIds = const [],
27 : int timeoutInSeconds = 1,
28 : ProgressCallback? progressCallback,
29 : bool resultsInAddressAscendingOrder = true,
30 : }) async* {
31 1 : final stream = getAllSendablePingableDevices(
32 : subnet,
33 : firstHostId: firstHostId,
34 : lastHostId: lastHostId,
35 : hostIds: hostIds,
36 : timeoutInSeconds: timeoutInSeconds,
37 : progressCallback: progressCallback,
38 : resultsInAddressAscendingOrder: resultsInAddressAscendingOrder,
39 : );
40 2 : await for (final sendableActiveHost in stream) {
41 1 : final activeHost = ActiveHost.fromSendableActiveHost(
42 : sendableActiveHost: sendableActiveHost,
43 : );
44 :
45 1 : await activeHost.resolveInfo();
46 :
47 : yield activeHost;
48 : }
49 : }
50 :
51 : /// Same as [getAllPingableDevices] but can be called or run inside isolate.
52 1 : @override
53 : Stream<SendableActiveHost> getAllSendablePingableDevices(
54 : String subnet, {
55 : int firstHostId = HostScannerService.defaultFirstHostId,
56 : int lastHostId = HostScannerService.defaultLastHostId,
57 : List<int> hostIds = const [],
58 : int timeoutInSeconds = 1,
59 : ProgressCallback? progressCallback,
60 : bool resultsInAddressAscendingOrder = true,
61 : }) async* {
62 1 : final int lastValidSubnet = validateAndGetLastValidSubnet(
63 : subnet,
64 : firstHostId,
65 : lastHostId,
66 : );
67 1 : final List<Future<SendableActiveHost?>> activeHostsFuture = [];
68 : final StreamController<SendableActiveHost> activeHostsController =
69 1 : StreamController<SendableActiveHost>();
70 :
71 1 : final List<int> pinged = [];
72 2 : for (int i = firstHostId; i <= lastValidSubnet; i++) {
73 2 : if (hostIds.isEmpty || hostIds.contains(i)) {
74 1 : pinged.add(i);
75 1 : activeHostsFuture.add(
76 1 : getHostFromPing(
77 : activeHostsController: activeHostsController,
78 1 : host: '$subnet.$i',
79 : timeoutInSeconds: timeoutInSeconds,
80 : ),
81 : );
82 : }
83 : }
84 :
85 : if (!resultsInAddressAscendingOrder) {
86 0 : yield* activeHostsController.stream;
87 : }
88 :
89 : int i = 0;
90 2 : for (final Future<SendableActiveHost?> host in activeHostsFuture) {
91 : final SendableActiveHost? tempHost = await host;
92 :
93 0 : progressCallback?.call(
94 0 : (pinged[i] - firstHostId) * 100 / (lastValidSubnet - firstHostId),
95 : );
96 1 : i++;
97 :
98 : if (tempHost == null) {
99 : continue;
100 : }
101 : yield tempHost;
102 : }
103 : }
104 :
105 1 : static PingResponse? _extractPingResponse(Object? event) {
106 1 : if (event is PingResponse) {
107 : return event;
108 : }
109 : if (event == null) {
110 : return null;
111 : }
112 :
113 : // dart_ping changed its stream payload type across versions.
114 : // Some versions emit PingSummary or other payloads instead of a response.
115 : try {
116 : final dynamic dynamicEvent = event;
117 : // ignore: avoid_dynamic_calls
118 1 : final Object? response = dynamicEvent.response;
119 0 : return response is PingResponse ? response : null;
120 : } catch (error) {
121 : return null;
122 : }
123 : }
124 :
125 1 : Future<SendableActiveHost?> getHostFromPing({
126 : required String host,
127 : required StreamController<SendableActiveHost> activeHostsController,
128 : int timeoutInSeconds = 1,
129 : }) async {
130 : SendableActiveHost? tempSendableActivateHost;
131 :
132 2 : await for (final Object? event in Ping(
133 : host,
134 : count: 1,
135 : timeout: timeoutInSeconds,
136 1 : forceCodepage: Platform.isWindows,
137 2 : ).stream) {
138 1 : final PingResponse? pingResponse = _extractPingResponse(event);
139 1 : if (pingResponse != null && pingResponse.time != null) {
140 : // Check if ping succeeded
141 3 : logger.fine("Pingable device found: $host");
142 1 : tempSendableActivateHost = SendableActiveHost(
143 : host,
144 : pingData: pingResponse,
145 : );
146 : } else {
147 3 : logger.fine("Non pingable device found: $host");
148 : }
149 :
150 : if (tempSendableActivateHost == null) {
151 : // Check if it's there in arp table
152 :
153 3 : final data = await getIt<Repository<ARPData>>().entryFor(host);
154 :
155 : if (data != null) {
156 0 : logger.fine("Successfully fetched arp entry for $host as $data");
157 0 : tempSendableActivateHost = SendableActiveHost(
158 : host,
159 : pingData: pingResponse,
160 : );
161 : } else {
162 3 : logger.fine("Problem in fetching arp entry for $host");
163 : }
164 : }
165 :
166 : if (tempSendableActivateHost != null) {
167 3 : logger.fine("Successfully added to result $host");
168 1 : activeHostsController.add(tempSendableActivateHost);
169 : }
170 : }
171 :
172 : return tempSendableActivateHost;
173 : }
174 :
175 1 : @override
176 : int validateAndGetLastValidSubnet(
177 : String subnet,
178 : int firstHostId,
179 : int lastHostId,
180 : ) {
181 1 : final int maxEnd = maxHost;
182 1 : if (firstHostId > lastHostId ||
183 1 : firstHostId < HostScannerService.defaultFirstHostId ||
184 1 : lastHostId < HostScannerService.defaultFirstHostId ||
185 1 : firstHostId > maxEnd ||
186 1 : lastHostId > maxEnd) {
187 : throw 'Invalid subnet range or firstHostId < lastHostId is not true';
188 : }
189 1 : return min(lastHostId, maxEnd);
190 : }
191 :
192 : /// Works same as [getAllPingableDevices] but does everything inside
193 : /// isolate out of the box.
194 1 : @override
195 : Stream<ActiveHost> getAllPingableDevicesAsync(
196 : String subnet, {
197 : int firstHostId = HostScannerService.defaultFirstHostId,
198 : int lastHostId = HostScannerService.defaultLastHostId,
199 : List<int> hostIds = const [],
200 : int timeoutInSeconds = 1,
201 : ProgressCallback? progressCallback,
202 : bool resultsInAddressAscendingOrder = true,
203 : }) async* {
204 : const int scanRangeForIsolate = 51;
205 1 : final int lastValidSubnet = validateAndGetLastValidSubnet(
206 : subnet,
207 : firstHostId,
208 : lastHostId,
209 : );
210 : for (
211 : int i = firstHostId;
212 1 : i <= lastValidSubnet;
213 2 : i += scanRangeForIsolate + 1
214 : ) {
215 2 : final limit = min(i + scanRangeForIsolate, lastValidSubnet);
216 3 : logger.fine('Scanning from $i to $limit');
217 :
218 1 : final receivePort = ReceivePort();
219 1 : final isolate = await Isolate.spawn(
220 : _startSearchingDevices,
221 1 : receivePort.sendPort,
222 : );
223 :
224 2 : await for (final message in receivePort) {
225 1 : if (message is SendPort) {
226 2 : message.send(<String>[
227 : subnet,
228 1 : i.toString(),
229 1 : limit.toString(),
230 1 : timeoutInSeconds.toString(),
231 1 : resultsInAddressAscendingOrder.toString(),
232 1 : dbDirectory,
233 2 : enableDebugging.toString(),
234 1 : hostIds.join(','),
235 : ]);
236 1 : } else if (message is SendableActiveHost) {
237 1 : final activeHostFound = ActiveHost.fromSendableActiveHost(
238 : sendableActiveHost: message,
239 : );
240 1 : await activeHostFound.resolveInfo();
241 2 : final j = int.tryParse(activeHostFound.hostId) ?? i;
242 0 : progressCallback?.call(
243 0 : (j - firstHostId) * 100 / (lastValidSubnet - firstHostId),
244 : );
245 : yield activeHostFound;
246 2 : } else if (message is String && message == 'Done') {
247 1 : isolate.kill();
248 : break;
249 : }
250 : }
251 : }
252 : }
253 :
254 : /// Will search devices in the network inside new isolate
255 1 : @pragma('vm:entry-point')
256 : static Future<void> _startSearchingDevices(SendPort sendPort) async {
257 1 : final port = ReceivePort();
258 2 : sendPort.send(port.sendPort);
259 :
260 2 : await for (final message in port) {
261 1 : if (message is List<String>) {
262 1 : final String subnetIsolate = message[0];
263 2 : final int firstSubnetIsolate = int.parse(message[1]);
264 2 : final int lastSubnetIsolate = int.parse(message[2]);
265 2 : final int timeoutInSeconds = int.parse(message[3]);
266 2 : final bool resultsInAddressAscendingOrder = message[4] == "true";
267 1 : final String dbDirectory = message[5];
268 2 : final bool enableDebugging = message[6] == "true";
269 1 : final String joinedIds = message[7];
270 : final List<int> hostIds = joinedIds
271 1 : .split(',')
272 3 : .where((e) => e.isNotEmpty)
273 1 : .map(int.parse)
274 1 : .toList();
275 : // configure again
276 1 : await configureNetworkTools(
277 : dbDirectory,
278 : enableDebugging: enableDebugging,
279 : );
280 :
281 : /// Will contain all the hosts that got discovered in the network, will
282 : /// be use inorder to cancel on dispose of the page.
283 : final Stream<SendableActiveHost> hostsDiscoveredInNetwork =
284 2 : HostScannerService.instance.getAllSendablePingableDevices(
285 : subnetIsolate,
286 : firstHostId: firstSubnetIsolate,
287 : lastHostId: lastSubnetIsolate,
288 : hostIds: hostIds,
289 : timeoutInSeconds: timeoutInSeconds,
290 : resultsInAddressAscendingOrder: resultsInAddressAscendingOrder,
291 : );
292 :
293 1 : await for (final SendableActiveHost activeHostFound
294 1 : in hostsDiscoveredInNetwork) {
295 1 : sendPort.send(activeHostFound);
296 : }
297 1 : sendPort.send('Done');
298 : }
299 : }
300 : }
301 :
302 : /// Scans for all hosts that have the specific port that was given.
303 : /// [resultsInAddressAscendingOrder] = false will return results faster but not in
304 : /// ascending order and without [progressCallback].
305 1 : @override
306 : Stream<ActiveHost> scanDevicesForSinglePort(
307 : String subnet,
308 : int port, {
309 : int firstHostId = HostScannerService.defaultFirstHostId,
310 : int lastHostId = HostScannerService.defaultLastHostId,
311 : Duration timeout = const Duration(milliseconds: 2000),
312 : ProgressCallback? progressCallback,
313 : bool resultsInAddressAscendingOrder = true,
314 : }) async* {
315 1 : final int lastValidSubnet = validateAndGetLastValidSubnet(
316 : subnet,
317 : firstHostId,
318 : lastHostId,
319 : );
320 1 : final List<Future<ActiveHost?>> activeHostOpenPortList = [];
321 : final StreamController<ActiveHost> activeHostsController =
322 1 : StreamController<ActiveHost>();
323 :
324 2 : for (int i = firstHostId; i <= lastValidSubnet; i++) {
325 1 : final host = '$subnet.$i';
326 1 : activeHostOpenPortList.add(
327 2 : PortScannerService.instance.connectToPort(
328 : address: host,
329 : port: port,
330 : timeout: timeout,
331 : activeHostsController: activeHostsController,
332 : ),
333 : );
334 : }
335 :
336 : if (!resultsInAddressAscendingOrder) {
337 0 : yield* activeHostsController.stream;
338 : }
339 :
340 : int counter = firstHostId;
341 : for (final Future<ActiveHost?> openPortActiveHostFuture
342 2 : in activeHostOpenPortList) {
343 : final ActiveHost? activeHost = await openPortActiveHostFuture;
344 : if (activeHost != null) {
345 : yield activeHost;
346 : }
347 0 : progressCallback?.call(
348 0 : (counter - firstHostId) * 100 / (lastValidSubnet - firstHostId),
349 : );
350 1 : counter++;
351 : }
352 : }
353 :
354 : /// Defines total number of subnets in class A network
355 : final classASubnets = 16777216;
356 :
357 : /// Defines total number of subnets in class B network
358 : final classBSubnets = 65536;
359 :
360 : /// Defines total number of subnets in class C network
361 : final classCSubnets = 256;
362 :
363 : /// Minimum value of first octet in IPv4 address used by getMaxHost
364 : final int minNetworkId = 1;
365 :
366 : /// Maximum value of first octect in IPv4 address used by getMaxHost
367 : final int maxNetworkId = 223;
368 :
369 : /// returns the max number of hosts a subnet can have excluding network Id and broadcast Id
370 0 : @Deprecated(
371 : "Implementation is wrong, since we only append in last octet, max host can only be 254. Use maxHost getter",
372 : )
373 : int getMaxHost(String subnet) {
374 0 : if (subnet.isEmpty) {
375 0 : throw ArgumentError('Invalid subnet address, address can not be empty.');
376 : }
377 0 : final List<String> firstOctetStr = subnet.split('.');
378 0 : if (firstOctetStr.isEmpty) {
379 0 : throw ArgumentError(
380 : 'Invalid subnet address, address should be in IPv4 format x.x.x',
381 : );
382 : }
383 :
384 0 : final int firstOctet = int.parse(firstOctetStr[0]);
385 :
386 0 : if (firstOctet >= minNetworkId && firstOctet < 128) {
387 0 : return classASubnets;
388 0 : } else if (firstOctet >= 128 && firstOctet < 192) {
389 0 : return classBSubnets;
390 0 : } else if (firstOctet >= 192 && firstOctet <= maxNetworkId) {
391 0 : return classCSubnets;
392 : }
393 : // Out of range for first octet
394 0 : throw RangeError.range(
395 : firstOctet,
396 0 : minNetworkId,
397 0 : maxNetworkId,
398 : 'subnet',
399 : 'Out of range for first octet',
400 : );
401 : }
402 :
403 1 : int get maxHost => HostScannerService.defaultLastHostId;
404 : }
|