LCOV - code coverage report
Current view: top level - lib/src/services/impls - port_scanner_service_impl.dart (source / functions) Coverage Total Hit
Test: coverage.lcov Lines: 91.0 % 89 81
Test Date: 2025-08-17 13:02:53 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:async';
       2              : import 'dart:isolate';
       3              : import 'dart:math';
       4              : 
       5              : import 'package:network_tools/network_tools.dart';
       6              : import 'package:universal_io/io.dart';
       7              : 
       8              : /// Scans open port for a target Address or domain.
       9              : class PortScannerServiceImpl extends PortScannerService {
      10              :   /// Checks if the single [port] is open or not for the [target].
      11              :   ///
      12              :   /// Returns an [ActiveHost] if the port is open, otherwise null.
      13              :   /// Throws if the port is out of range or the target cannot be resolved.
      14            1 :   @override
      15              :   Future<ActiveHost?> isOpen(
      16              :     String target,
      17              :     int port, {
      18              :     Duration timeout = const Duration(milliseconds: 2000),
      19              :   }) async {
      20            2 :     if (port < 0 || port > 65535) {
      21              :       throw 'Provide a valid port range between '
      22              :           '0 to 65535 or startPort < endPort is not true';
      23              :     }
      24            1 :     final List<InternetAddress> address = await InternetAddress.lookup(
      25              :       target,
      26              :       type: InternetAddressType.IPv4,
      27              :     );
      28            1 :     if (address.isNotEmpty) {
      29            2 :       final String hostAddress = address[0].address;
      30            1 :       return connectToPort(
      31            1 :         activeHostsController: StreamController<ActiveHost>(),
      32              :         address: hostAddress,
      33              :         port: port,
      34              :         timeout: timeout,
      35              :       );
      36              :     } else {
      37              :       throw 'Name can not be resolved';
      38              :     }
      39              :   }
      40              : 
      41              :   /// Scans only the ports listed in [portList] for a [target].
      42              :   ///
      43              :   /// Progress can be retrieved by [progressCallback].
      44              :   /// Tries connecting ports until [timeout] is reached for each port.
      45              :   /// If [resultsInAddressAscendingOrder] is false, results may be returned faster but not in ascending order and without [progressCallback].
      46              :   /// If [async] is true, scanning is performed in isolates for better performance on large port lists.
      47            1 :   @override
      48              :   Stream<ActiveHost> customDiscover(
      49              :     String target, {
      50              :     List<int> portList = PortScannerService.commonPorts,
      51              :     ProgressCallback? progressCallback,
      52              :     Duration timeout = const Duration(milliseconds: 2000),
      53              :     bool resultsInAddressAscendingOrder = true,
      54              :     bool async = false,
      55              :   }) async* {
      56              :     if (!async) {
      57              :       // If async is false then run on main isolate
      58            1 :       yield* _customDiscover<ActiveHost>(
      59              :         target,
      60              :         portList: portList,
      61              :         progressCallback: progressCallback,
      62              :         timeout: timeout,
      63              :         resultsInAddressAscendingOrder: resultsInAddressAscendingOrder,
      64              :       );
      65              :     } else {
      66              :       const int scanRangeForIsolate = 1000;
      67            4 :       for (int i = 0; i <= portList.length; i += scanRangeForIsolate + 1) {
      68            3 :         final limit = min(i + scanRangeForIsolate, portList.length);
      69            1 :         final receivePort = ReceivePort();
      70            1 :         final isolate = await Isolate.spawn(
      71            1 :           _startSearchingPorts,
      72            1 :           receivePort.sendPort,
      73              :         );
      74              : 
      75            2 :         await for (final message in receivePort) {
      76            1 :           if (message is SendPort) {
      77            2 :             message.send(<dynamic>[
      78              :               target,
      79            1 :               portList.sublist(i, limit),
      80              :               timeout,
      81            1 :               resultsInAddressAscendingOrder.toString(),
      82            1 :               dbDirectory,
      83            2 :               enableDebugging.toString(),
      84              :             ]);
      85            1 :           } else if (message is SendableActiveHost) {
      86            0 :             progressCallback?.call(i * 100 / (portList.length));
      87            1 :             final activeHostFound = ActiveHost.fromSendableActiveHost(
      88              :               sendableActiveHost: message,
      89              :             );
      90            1 :             await activeHostFound.resolveInfo();
      91              :             yield activeHostFound;
      92            2 :           } else if (message is String && message == 'Done') {
      93            1 :             isolate.kill();
      94              :             break;
      95              :           }
      96              :         }
      97              :       }
      98              :     }
      99              :   }
     100              : 
     101            1 :   @pragma('vm:entry-point')
     102              :   Future<void> _startSearchingPorts(SendPort sendPort) async {
     103            1 :     final port = ReceivePort();
     104            2 :     sendPort.send(port.sendPort);
     105              : 
     106            2 :     await for (final message in port) {
     107            1 :       if (message is List<dynamic>) {
     108            1 :         final String target = message[0] as String;
     109            1 :         final List<int> portList = message[1] as List<int>;
     110            1 :         final Duration timeout = message[2] as Duration;
     111            2 :         final bool resultsInAddressAscendingOrder = message[3] == "true";
     112            1 :         final String dbDirectory = message[4] as String;
     113            2 :         final bool enableDebugging = message[5] == "true";
     114              :         // configure again
     115            1 :         await configureNetworkTools(
     116              :           dbDirectory,
     117              :           enableDebugging: enableDebugging,
     118              :         );
     119            1 :         final openPortsForTarget = _customDiscover<SendableActiveHost>(
     120              :           target,
     121              :           portList: portList,
     122              :           timeout: timeout,
     123              :           resultsInAddressAscendingOrder: resultsInAddressAscendingOrder,
     124              :         );
     125              : 
     126            1 :         await for (final SendableActiveHost activeHostFound
     127            1 :             in openPortsForTarget) {
     128            1 :           sendPort.send(activeHostFound);
     129              :         }
     130            1 :         sendPort.send('Done');
     131              :       }
     132              :     }
     133              :   }
     134              : 
     135            1 :   Stream<T> _customDiscover<T>(
     136              :     String target, {
     137              :     List<int> portList = PortScannerService.commonPorts,
     138              :     ProgressCallback? progressCallback,
     139              :     Duration timeout = const Duration(milliseconds: 2000),
     140              :     bool resultsInAddressAscendingOrder = true,
     141              :   }) async* {
     142            1 :     final List<InternetAddress> address = await InternetAddress.lookup(
     143              :       target,
     144              :       type: InternetAddressType.IPv4,
     145              :     );
     146            1 :     if (address.isNotEmpty) {
     147            2 :       final String hostAddress = address[0].address;
     148            1 :       final List<Future<T?>> openPortList = [];
     149            1 :       final StreamController<T> activeHostsController = StreamController<T>();
     150              : 
     151            3 :       for (int k = 0; k < portList.length; k++) {
     152            4 :         if (portList[k] >= 0 && portList[k] <= 65535) {
     153            1 :           openPortList.add(
     154            1 :             _connectToPort<T>(
     155              :               address: hostAddress,
     156            1 :               port: portList[k],
     157              :               timeout: timeout,
     158              :               activeHostsController: activeHostsController,
     159              :             ),
     160              :           );
     161              :         }
     162              :       }
     163              : 
     164              :       if (!resultsInAddressAscendingOrder) {
     165            0 :         yield* activeHostsController.stream;
     166              :       }
     167              : 
     168              :       int counter = 0;
     169              : 
     170            2 :       for (final Future<T?> openPortFuture in openPortList) {
     171              :         final T? openPort = await openPortFuture;
     172              :         if (openPort == null) {
     173              :           continue;
     174              :         }
     175            0 :         progressCallback?.call(counter * 100 / portList.length);
     176              :         yield openPort;
     177            1 :         counter++;
     178              :       }
     179              :     } else {
     180              :       throw 'Name can not be resolved';
     181              :     }
     182              :   }
     183              : 
     184              :   /// Scans ports from [startPort] to [endPort] for a [target].
     185              :   ///
     186              :   /// Progress can be retrieved by [progressCallback].
     187              :   /// Tries connecting ports until [timeout] is reached for each port.
     188              :   /// If [resultsInAddressAscendingOrder] is false, results may be returned faster but not in ascending order and without [progressCallback].
     189              :   /// If [async] is true, scanning is performed in isolates for better performance on large port ranges.
     190            1 :   @override
     191              :   Stream<ActiveHost> scanPortsForSingleDevice(
     192              :     String target, {
     193              :     int startPort = PortScannerService.defaultStartPort,
     194              :     int endPort = PortScannerService.defaultEndPort,
     195              :     ProgressCallback? progressCallback,
     196              :     Duration timeout = const Duration(milliseconds: 2000),
     197              :     bool resultsInAddressAscendingOrder = true,
     198              :     bool async = false,
     199              :   }) async* {
     200            1 :     if (startPort < 0 ||
     201            1 :         endPort < 0 ||
     202            1 :         startPort > 65535 ||
     203            1 :         endPort > 65535 ||
     204            1 :         startPort > endPort) {
     205              :       throw 'Provide a valid port range between 0 to 65535 or startPort <'
     206              :           ' endPort is not true';
     207              :     }
     208              : 
     209            1 :     final List<int> portList = [];
     210              : 
     211            2 :     for (int i = startPort; i <= endPort; ++i) {
     212            1 :       portList.add(i);
     213              :     }
     214              : 
     215            1 :     yield* customDiscover(
     216              :       target,
     217              :       portList: portList,
     218              :       progressCallback: progressCallback,
     219              :       timeout: timeout,
     220              :       resultsInAddressAscendingOrder: resultsInAddressAscendingOrder,
     221              :       async: async,
     222              :     );
     223              :   }
     224              : 
     225              :   /// Attempts to connect to a specific [port] on an [address] within a [timeout].
     226              :   ///
     227              :   /// Returns an [ActiveHost] if the port is open, otherwise null. Used internally.
     228            1 :   @override
     229              :   Future<ActiveHost?> connectToPort({
     230              :     required String address,
     231              :     required int port,
     232              :     required Duration timeout,
     233              :     required StreamController<ActiveHost> activeHostsController,
     234              :     int recursionCount = 0,
     235              :   }) async {
     236            1 :     return await _connectToPort<ActiveHost>(
     237              :       address: address,
     238              :       port: port,
     239              :       timeout: timeout,
     240              :       activeHostsController: activeHostsController,
     241              :     );
     242              :   }
     243              : 
     244            1 :   Future<T?> _connectToPort<T>({
     245              :     required String address,
     246              :     required int port,
     247              :     required Duration timeout,
     248              :     required StreamController<T> activeHostsController,
     249              :     int recursionCount = 0,
     250              :   }) async {
     251              :     try {
     252            1 :       final Socket s = await Socket.connect(address, port, timeout: timeout);
     253            1 :       s.destroy();
     254              : 
     255            1 :       if (T == SendableActiveHost) {
     256            1 :         final SendableActiveHost sendableActiveHost = SendableActiveHost(
     257              :           address,
     258            2 :           openPorts: [OpenPort(port)],
     259              :         );
     260            1 :         activeHostsController.add(sendableActiveHost as T);
     261              :         return sendableActiveHost as T;
     262              :       }
     263            1 :       final ActiveHost activeHost = ActiveHost.buildWithAddress(
     264              :         address: address,
     265            2 :         openPorts: [OpenPort(port)],
     266              :       );
     267            1 :       activeHostsController.add(activeHost as T);
     268              :       return activeHost as T;
     269              :     } catch (e) {
     270            1 :       if (e is! SocketException) {
     271              :         rethrow;
     272              :       }
     273              : 
     274              :       // Check if connection timed out or we got one of predefined errors
     275            5 :       if (e.osError == null || _errorCodes.contains(e.osError?.errorCode)) {
     276              :         return null;
     277              :       }
     278              : 
     279              :       // Error 23,24: Too many open files in system
     280              :       // e.osError can't be null here so `!` can be used
     281              :       // Do no more than 2 retries to prevent infinite loops
     282            0 :       if (recursionCount < 3 &&
     283            0 :           (e.osError!.errorCode == 23 || e.osError!.errorCode == 24)) {
     284              :         // Hotfix: Wait for the timeout (+ a little more) to complete and retry
     285              :         // -> Other connections must be closed now and the file handles available again
     286              : 
     287            0 :         await Future.delayed(timeout + const Duration(milliseconds: 250));
     288              : 
     289            0 :         return _connectToPort<T>(
     290              :           address: address,
     291              :           port: port,
     292              :           timeout: timeout,
     293              :           activeHostsController: activeHostsController,
     294            0 :           recursionCount: recursionCount + 1,
     295              :         );
     296              :       }
     297              : 
     298              :       rethrow;
     299              :     }
     300              :   }
     301              : 
     302              :   final _errorCodes = [13, 49, 61, 64, 65, 101, 110, 111, 113, 10060];
     303              : }
        

Generated by: LCOV version 2.0-1