<?php
namespace SHServ\Tools;
/**
* DeviceScanner — инструмент для сканирования локальной сети
* и поиска устройств, отвечающих JSON на /about.
*
* Требует расширение php-curl.
*/
class DeviceScanner {
/**
* Сканирует диапазон IP.
*
* @param string $start_ip Начальный IP (например "192.168.1.1")
* @param string $end_ip Конечный IP (например "192.168.1.254")
* @param int $port HTTP порт (по умолчанию 80)
* @param float $timeout Таймаут на IP (сек)
* @return array Найденные устройства
*/
public function scan_range(string $start_ip, string $end_ip, int $port = 80, float $timeout = 1): array {
$ips = $this -> generate_ip_range($start_ip, $end_ip);
if (empty($ips)) {
return [];
}
return $this -> scan_ips($ips, $port, $timeout);
}
/**
* Фильтр по статусу.
*
* @param array $devices
* @param string|string[] $status
* @return array
*/
public function filter_by_status(array $devices, $status): array {
$statuses = is_array($status) ? $status : [$status];
$statuses = array_map('strval', $statuses);
return array_values(array_filter($devices, function ($device) use ($statuses) {
if (!isset($device['status'])) {
return false;
}
return in_array((string)$device['status'], $statuses, true);
}));
}
/**
* Генерация диапазона IP (start..end включительно).
*/
private function generate_ip_range(string $start_ip, string $end_ip): array {
$start = ip2long($start_ip);
$end = ip2long($end_ip);
if ($start === false || $end === false || $start > $end) {
return [];
}
$ips = [];
for ($ip_long = $start; $ip_long <= $end; $ip_long++) {
$ips[] = long2ip($ip_long);
}
return $ips;
}
/**
* Параллельное сканирование IP с curl_multi.
*/
private function scan_ips(array $ips, int $port, float $timeout): array {
$multi = curl_multi_init();
$handles = [];
foreach ($ips as $ip) {
$url = "http://{$ip}:{$port}/about";
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => $timeout,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_NOBODY => false,
CURLOPT_HEADER => false,
]);
curl_multi_add_handle($multi, $ch);
// тут больше НЕ приводим $ch к строке
$handles[] = [
'handle' => $ch,
'ip' => $ip,
'url' => $url,
];
}
$running = null;
do {
$status = curl_multi_exec($multi, $running);
if ($status > 0) {
break;
}
// на некоторых системах curl_multi_select может возвращать -1
$select_result = curl_multi_select($multi, 0.1);
if ($select_result === -1) {
// маленькая пауза, чтобы не крутить цикл
usleep(100000);
}
} while ($running > 0);
$devices = [];
foreach ($handles as $item) {
$ch = $item['handle'];
$ip = $item['ip'];
$url = $item['url'];
$body = curl_multi_getcontent($ch);
$curl_err = curl_errno($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_multi_remove_handle($multi, $ch);
curl_close($ch);
if ($curl_err !== 0 || $http !== 200 || !$body) {
continue;
}
$json = json_decode($body, true);
if (!is_array($json)) {
continue;
}
if (!isset($json['ip_address'])) {
$json['ip_address'] = $ip;
}
if (!isset($json['server'])) {
$json['server'] = "http://{$ip}";
}
$json['_meta'] = [
'ip' => $ip,
'url' => $url,
'http_code' => $http,
];
$devices[] = $json;
}
curl_multi_close($multi);
return $devices;
}
}