Рефакторинг. docstring

This commit is contained in:
parent 060f4de175
commit 37f9e3abfb
14 changed files with 1234 additions and 243 deletions

View file

@ -5,10 +5,21 @@ from netaddr import IPNetwork
class NetworkScanner:
"""Иницилазицая сетевого сканера"""
"""Инициализация сетевого сканера.
Предоставляет методы для сканирования сети с использованием ARP-запросов и
вывода информации о клиентах.
"""
def __init__(self, interface=None):
"""Инициализация параметров интерфейсов"""
"""Инициализация параметров сетевого сканера.
Args:
interface (str, optional): Название сетевого интерфейса. По умолчанию
используется интерфейс по умолчанию из netifaces.gateways().
Инициализирует интерфейс, информацию об IP-адресах и локальный MAC-адрес.
"""
self.interface = (
interface or netifaces.gateways()["default"][netifaces.AF_INET][1])
self.ip_info = netifaces.ifaddresses(self.interface).get(
@ -20,13 +31,34 @@ class NetworkScanner:
self.interface)[netifaces.AF_LINK][0]["addr"]
def _create_arp_packet(self, target_ip):
"""Создание пакета ARP для отправки"""
"""Создаёт ARP-пакет для отправки на указанный IP-адрес.
Args:
target_ip (str): Целевой IP-адрес или подсеть для сканирования.
Returns:
scapy.packet.Packet: Сформированный ARP-пакет с Ethernet-заголовком.
"""
arp = ARP(pdst=target_ip)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
return ether / arp
def scan_network(self, attempts=3, timeout=2):
"""Подготовка к сканированию сети"""
"""Сканирует сеть для обнаружения активных клиентов.
Args:
attempts (int, optional): Количество попыток отправки ARP-запросов.
По умолчанию 3.
timeout (int, optional): Таймаут ожидания ответа в секундах.
По умолчанию 2.
Returns:
List[dict]: Список словарей с ключами "ip" и "mac", представляющих
обнаруженных клиентов, включая локальные IP-адреса.
Note:
Учитывает локальные IP-адреса интерфейса и удаляет дубликаты.
"""
all_clients = []
for network in self.networks:
@ -41,30 +73,35 @@ class NetworkScanner:
verbose=0)[0]
for _, received in result:
client = {"ip": received.psrc, "mac": received.hwsrc}
if client not in clients: # Избегаем дублирования клиентов
if client not in clients:
clients.append(client)
all_clients.extend(clients)
# Добавляет все локальные IP-адреса интерфейса как клиенты
for ip_info in self.ip_info:
local_client = {"ip": ip_info["addr"], "mac": self.local_mac}
if local_client not in all_clients:
all_clients.append(local_client)
# Удаление дубликатов
unique_clients = {tuple(client.items()) for client in all_clients}
return [dict(client) for client in unique_clients]
def print_clients(self, clients):
"""Лист адресов клиентов"""
"""Выводит список адресов клиентов в формате таблицы.
Args:
clients (List[dict]): Список словарей с ключами "ip" и "mac",
представляющих клиентов.
Выводит IP-адреса и MAC-адреса в отсортированном по IP порядке.
"""
print("Доступные устройства в сети:")
print("IP" + " " * 18 + "MAC")
for client in sorted(clients, key=lambda x: x["ip"]):
print(f"{client['ip']:<16} {client['mac']}")
# Оставил нативный запуск сервиса для тестов
if __name__ == "__main__":
scanner = NetworkScanner()
clients = scanner.scan_network()
scanner.print_clients(clients)
scanner.print_clients(clients)

View file

@ -1,4 +1,3 @@
# services/auth_service.py
from jose import JWTError, jwt
from datetime import datetime, timedelta
from fastapi import HTTPException, Depends, Request

View file

@ -6,11 +6,28 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(level
logger = logging.getLogger(__name__)
class WifiConnectionService:
"""Сервис для управления подключением к Wi-Fi сетям с использованием nmcli.
Предоставляет методы для подключения, отключения и проверки статуса Wi-Fi соединений.
"""
def __init__(self):
"""Инициализирует сервис Wi-Fi.
Автоматически определяет доступный Wi-Fi-интерфейс и сохраняет его имя.
"""
self.interface = self._get_wifi_interface()
def _get_wifi_interface(self) -> str:
"""Автоматически определяет Wi-Fi-интерфейс."""
"""Автоматически определяет Wi-Fi-интерфейс.
Returns:
str: Название Wi-Fi-интерфейса (например, "wlan0") или пустая строка,
если интерфейс не найден или Wi-Fi отключён.
Raises:
Exception: Если произошла ошибка при выполнении команд nmcli.
"""
try:
result = subprocess.run(["nmcli", "radio", "wifi"], capture_output=True, text=True)
if "enabled" in result.stdout.lower():
@ -29,7 +46,21 @@ class WifiConnectionService:
return ""
def connect(self, ssid: str, password: str) -> Tuple[bool, Optional[str]]:
"""Подключается к Wi-Fi сети по SSID и паролю."""
"""Подключается к Wi-Fi сети по SSID и паролю.
Args:
ssid (str): Идентификатор сети Wi-Fi.
password (str): Пароль для подключения.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
subprocess.CalledProcessError: Если команда nmcli завершилась с ошибкой.
Exception: Если произошла неизвестная ошибка при выполнении.
"""
try:
if not self.interface:
return False, "Wi-Fi-интерфейс не доступен"
@ -63,7 +94,20 @@ class WifiConnectionService:
return False, f"Ошибка подключения к {ssid}: Неизвестная ошибка - {str(e)}"
def disconnect(self, ssid: str) -> Tuple[bool, Optional[str]]:
"""Отключается от Wi-Fi сети по указанному SSID."""
"""Отключается от Wi-Fi сети по указанному SSID.
Args:
ssid (str): Идентификатор сети Wi-Fi для отключения.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None или сообщение, если подключение не найдено).
- Ошибка: (False, сообщение об ошибке).
Raises:
subprocess.CalledProcessError: Если команда nmcli завершилась с ошибкой.
Exception: Если произошла неизвестная ошибка при выполнении.
"""
try:
if not self.interface:
return False, "Wi-Fi-интерфейс не доступен"
@ -91,7 +135,16 @@ class WifiConnectionService:
return False, f"Ошибка отключения от {ssid}: {str(e)}"
def get_status(self) -> Tuple[bool, Optional[str]]:
"""Проверяет статус текущего подключения к Wi-Fi."""
"""Проверяет статус текущего подключения к Wi-Fi.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, SSID или сообщение об ошибке).
- Успех: (True, SSID текущей сети).
- Ошибка или отсутствие подключения: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при выполнении команды nmcli.
"""
try:
if not self.interface:
return False, "Wi-Fi-интерфейс не доступен"

View file

@ -1,4 +1,3 @@
import asyncio
from typing import Dict, Any
from sdbus_async.networkmanager import (
NetworkManager,
@ -12,17 +11,28 @@ from services.arp_service import NetworkScanner
class IPService:
"""
Сервис проверки доступности IP-адреса и применения настроек сети
"""Сервис проверки доступности IP-адреса и применения настроек сети.
Предоставляет асинхронные методы для проверки занятости IP-адреса с использованием
ARP-сканирования и применения настроек через NetworkManager.
"""
def __init__(self):
"""Инициализирует сервис IP.
Создаёт объект ARP-сканера и устанавливает системную шину D-Bus как
по умолчанию.
"""
self.arp_scanner = NetworkScanner()
self.bus = sdbus.sd_bus_open_system()
sdbus.set_default_bus(self.bus)
def _reset_bus(self):
"""Перезапускает соединение с системной шиной D-Bus"""
"""Перезапускает соединение с системной шиной D-Bus.
Закрывает текущее соединение и создаёт новое, устанавливая его как
по умолчанию.
"""
new_bus = sdbus.sd_bus_open_system()
sdbus.set_default_bus(new_bus)
if hasattr(self, "bus") and self.bus is not None:
@ -30,7 +40,23 @@ class IPService:
self.bus = new_bus
async def check_ip_config(self, args: Dict[str, Any]) -> Dict[str, Any]:
"""Проверяет доступность IP-адреса перед его применением"""
"""Проверяет доступность IP-адреса перед его применением.
Выполняет ARP-сканирование сети для проверки, занят ли указанный IP-адрес.
Перезапускает шину D-Bus перед выполнением.
Args:
args (Dict[str, Any]): Словарь с данными, включая "ipv4_updates"
с полем "ipv4.address-data" для нового IP-адреса.
Returns:
Dict[str, Any]: Словарь с ключами:
- "available" (bool): Доступен ли IP-адрес.
- "message" (str): Сообщение о результате проверки.
Raises:
Exception: Если произошла ошибка при сканировании ARP.
"""
self._reset_bus()
clients = self.arp_scanner.scan_network()
new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"]
@ -45,7 +71,23 @@ class IPService:
}
async def apply_ip_config(self, args: Dict[str, Any]) -> None:
"""Применяет проверенные настройки IP"""
"""Применяет проверенные настройки IP к указанному соединению.
Перезапускает шину D-Bus, обновляет настройки соединения через NetworkManager,
отключает и переподключает устройство к сети.
Args:
args (Dict[str, Any]): Словарь с данными, включая:
- "connection_id" (str): Идентификатор соединения.
- "ipv4_updates" (Dict): Обновления настроек IPv4.
Raises:
ValueError: Если соединение с указанным ID не найдено.
RuntimeError: Если устройство для интерфейса не найдено.
Note:
Требуются права для изменения настроек через NetworkManager.
"""
self._reset_bus()
nm = NetworkManager()
settings = NetworkManagerSettings()
@ -77,10 +119,8 @@ class IPService:
if await device.interface == interface_name:
await device.disconnect()
print(f"Отключён от {interface_name}")
await nm.activate_connection(connection=connection_paths[0],
device=device_path)
await nm.activate_connection(connection=connection_paths[0],device=device_path)
print(f"Переподключён к {args['connection_id']}")
return
raise RuntimeError(
f"Устройство для интерфейса {interface_name} не найдено")
raise RuntimeError(f"Устройство для интерфейса {interface_name} не найдено")

View file

@ -23,16 +23,35 @@ import subprocess
class NetworkService:
"""Сервис для взаимодействия с NetworkManager через D-Bus"""
"""Сервис для взаимодействия с NetworkManager через D-Bus.
Предоставляет асинхронные методы для получения информации о сетевых интерфейсах,
их конфигурациях IP, Wi-Fi и состояниях.
"""
def __init__(self):
"""Инициализирует сервис NetworkManager.
Устанавливает системный D-Bus и создаёт объекты для взаимодействия с
NetworkManager и его настройками.
"""
self.system_bus = sd_bus_open_system()
self.nm = NetworkManager(self.system_bus)
self.nm_settings = NetworkManagerSettings(self.system_bus)
async def get_ip_method(self, device: NetworkDeviceGeneric,
ip_type: str) -> str:
"""Получает метод IP для устройства"""
async def get_ip_method(self, device: NetworkDeviceGeneric, ip_type: str) -> str:
"""Получает метод IP (например, "auto", "manual") для указанного устройства.
Args:
device (NetworkDeviceGeneric): Объект устройства NetworkManager.
ip_type (str): Тип IP (например, "ipv4" или "ipv6").
Returns:
str: Метод IP или "unknown" в случае ошибки.
Raises:
Exception: Если произошла ошибка при получении настроек соединения.
"""
try:
config = await device.get_applied_connection()
settings = config[0].get(ip_type, {})
@ -41,64 +60,80 @@ class NetworkService:
return "unknown"
async def get_connection_name(self, device: NetworkDeviceGeneric) -> str:
"""Извлекает имя соединения (GENERAL.CONNECTION) для устройства"""
"""Извлекает имя соединения (GENERAL.CONNECTION) для устройства.
Args:
device (NetworkDeviceGeneric): Объект устройства NetworkManager.
Returns:
str: Имя соединения или "unknown" в случае ошибки.
Raises:
Exception: Если произошла ошибка при получении настроек соединения.
"""
try:
applied_connection = await device.get_applied_connection()
connection_settings = applied_connection[0]
connection_info = connection_settings.get("connection", {})
return str(connection_info.get("id", ("s", "unknown"))[1])
except Exception as e:
# print(f"Ошибка при получении соединения для устройства {await device.interface}: {e}")
# срёт ошибки дл отключенных интерфейсов
# добавить потом условия для отключенных интерфейсов
return "unknown"
async def get_ip_config(self, device: NetworkDeviceGeneric,
ip_version: str) -> List[IndividualIPConfig]:
"""Получает конфигурацию IP для устройства"""
async def get_ip_config(self, device: NetworkDeviceGeneric, ip_version: str) -> List[IndividualIPConfig]:
"""Получает конфигурацию IP для устройства.
Args:
device (NetworkDeviceGeneric): Объект устройства NetworkManager.
ip_version (str): Версия IP ("4" или "6").
Returns:
List[IndividualIPConfig]: Список конфигураций IP, включая адреса и маршруты.
Raises:
Exception: Если произошла ошибка при получении данных IP.
"""
configs = []
device_ip = await getattr(device, f"ip{ip_version}_config")
if device_ip != "/":
ip_config = globals()[f"IPv{ip_version}Config"](device_ip,
self.system_bus)
ip_config = globals()[f"IPv{ip_version}Config"](device_ip, self.system_bus)
address_data = await ip_config.address_data
for address in address_data:
config = IndividualIPConfig(
address=str(address["address"][1]),
routes=[
globals()[f"IPv{ip_version}Route"](
destination=route.get(
"dest",
("s",
"0.0.0.0" if ip_version == "4" else "::"))[1],
destination=route.get("dest", ("s", "0.0.0.0" if ip_version == "4" else "::"))[1],
prefix=route.get("prefix", ("u", 0))[1],
next_hop=route.get(
"next-hop",
("s",
"0.0.0.0" if ip_version == "4" else "::"),
)[1],
next_hop=route.get("next-hop", ("s", "0.0.0.0" if ip_version == "4" else "::"))[1],
metric=route.get("metric", ("u", 0))[1],
) for route in await ip_config.route_data
],
method=await self.get_ip_method(device,
f"ipv{ip_version}"),
method=await self.get_ip_method(device, f"ipv{ip_version}"),
gateway=str(await ip_config.gateway),
dns=[
str(ns["address"][1]) if ip_version == "4" else str(ns)
for ns in await getattr(
ip_config,
"nameserver_data" if ip_version ==
"4" else "nameservers",
"nameserver_data" if ip_version == "4" else "nameservers",
)
],
)
configs.append(config)
return configs
async def get_wifi_info(
self, wifi_device: NetworkDeviceWireless
) -> Dict[str, Optional[Union[str, int]]]:
"""Получает информацию о Wi-Fi устройстве"""
async def get_wifi_info(self, wifi_device: NetworkDeviceWireless) -> Dict[str, Optional[Union[str, int]]]:
"""Получает информацию о Wi-Fi устройстве.
Args:
wifi_device (NetworkDeviceWireless): Объект Wi-Fi устройства NetworkManager.
Returns:
Dict[str, Optional[Union[str, int]]]: Словарь с данными Wi-Fi (SSID, BSSID,
bitrate, strength, frequency, mode), где значения могут быть None.
Raises:
Exception: Если произошла ошибка при получении данных точки доступа.
"""
active_ap = await wifi_device.active_access_point
wifi_info = {
"ssid": None,
@ -123,7 +158,18 @@ class NetworkService:
self,
initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[Union[NetworkInterface, WiFiDevice]]:
"""Получает список сетевых интерфейсов"""
"""Получает список сетевых интерфейсов.
Args:
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
координат (x, y) для интерфейсов. По умолчанию None (используются (100, 100)).
Returns:
List[Union[NetworkInterface, WiFiDevice]]: Список объектов интерфейсов,
включая Wi-Fi устройства.
Исключает интерфейсы типа LOOPBACK и неактивные состояния (state == "10").
"""
devices_paths = await self.nm.get_devices()
network_interfaces = []
initial_positions = initial_positions or {}
@ -141,43 +187,37 @@ class NetworkService:
continue
common_data = {
"name":
interface_name,
"type":
device_type,
"device_state":
device_state,
"active_connection":
await self.get_connection_name(generic_device),
"mac_address":
str(await generic_device.hw_address),
"device_mtu":
str(await generic_device.mtu),
"ipv4":
IPConfig(configurations=await self.get_ip_config(
generic_device, "4")),
"ipv6":
IPConfig(configurations=await self.get_ip_config(
generic_device, "6")),
"x":
initial_positions.get(interface_name, (100, 100))[0],
"y":
initial_positions.get(interface_name, (100, 100))[1],
"name": interface_name,
"type": device_type,
"device_state": device_state,
"active_connection": await self.get_connection_name(generic_device),
"mac_address": str(await generic_device.hw_address),
"device_mtu": str(await generic_device.mtu),
"ipv4": IPConfig(configurations=await self.get_ip_config(generic_device, "4")),
"ipv6": IPConfig(configurations=await self.get_ip_config(generic_device, "6")),
"x": initial_positions.get(interface_name, (100, 100))[0],
"y": initial_positions.get(interface_name, (100, 100))[1],
}
if device_type == "WIFI":
wifi_device = NetworkDeviceWireless(device_path,
self.system_bus)
wifi_device = NetworkDeviceWireless(device_path, self.system_bus)
wifi_info = await self.get_wifi_info(wifi_device)
network_interfaces.append(
WiFiDevice(**common_data, **wifi_info))
network_interfaces.append(WiFiDevice(**common_data, **wifi_info))
else:
network_interfaces.append(NetworkInterface(**common_data))
return network_interfaces
async def get_hostname(self) -> Dict[str, str]:
"""Возвращает имя хоста из команды hostname в формате JSON"""
"""Возвращает имя хоста из команды hostname в формате JSON.
Returns:
Dict[str, str]: Словарь с ключом "hostname" и значением имени хоста.
Raises:
RuntimeError: Если произошла ошибка при выполнении команды hostname
или обработке вывода.
"""
try:
result = subprocess.run(["hostname"],
capture_output=True,
@ -195,8 +235,15 @@ class NetworkService:
state: str,
initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[NetworkInterface]:
"""
Получает список интерфейсов по состоянию
"""Получает список интерфейсов по указанному состоянию.
Args:
state (str): Состояние интерфейса (например, "100" для активных).
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
координат (x, y) для интерфейсов. По умолчанию None.
Returns:
List[NetworkInterface]: Список интерфейсов с указанным состоянием.
"""
all_interfaces = await self.get_network_interfaces(initial_positions)
return [
@ -211,10 +258,21 @@ class NetworkService:
container_x: int = 0,
container_y: int = 0,
) -> NetworkInterfacesContainer:
"""Получает объект, содержащий список интерфейсов и координаты контейнера"""
"""Получает объект, содержащий список интерфейсов и координаты контейнера.
Args:
state (Optional[str], optional): Состояние интерфейсов (например, "100").
По умолчанию None (все интерфейсы).
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
координат (x, y) для интерфейсов. По умолчанию None.
container_x (int, optional): Координата X контейнера. По умолчанию 0.
container_y (int, optional): Координата Y контейнера. По умолчанию 0.
Returns:
NetworkInterfacesContainer: Объект с интерфейсами и координатами контейнера.
"""
if state:
interfaces = await self.get_interfaces_by_state(
state, initial_positions)
interfaces = await self.get_interfaces_by_state(state, initial_positions)
else:
interfaces = await self.get_network_interfaces(initial_positions)
return NetworkInterfacesContainer(interfaces=interfaces,
@ -225,7 +283,15 @@ class NetworkService:
self,
initial_positions: Optional[Dict[str, Tuple[int, int]]] = None,
) -> NetworkInterfacesContainer:
"""Получает контейнер с сетевыми интерфейсами и координатами контейнера"""
"""Получает контейнер с сетевыми интерфейсами и координатами контейнера.
Args:
initial_positions (Optional[Dict[str, Tuple[int, int]]], optional):
Словарь начальных координат (x, y) для интерфейсов. По умолчанию None.
Returns:
NetworkInterfacesContainer: Объект с интерфейсами и координатами (0, 0).
"""
interfaces = await self.get_network_interfaces(initial_positions)
return NetworkInterfacesContainer(interfaces=interfaces,
container_x=0,
@ -235,14 +301,30 @@ class NetworkService:
self,
initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[NetworkInterface]:
"""Получает список активных сетевых интерфейсов"""
"""Получает список активных сетевых интерфейсов.
Args:
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
координат (x, y) для интерфейсов. По умолчанию None.
Returns:
List[NetworkInterface]: Список активных интерфейсов (state == "100").
"""
return await self.get_interfaces_by_state("100", initial_positions)
async def get_deactivated_interfaces(
self,
initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[NetworkInterface]:
"""Получает список неактивных сетевых интерфейсов"""
"""Получает список неактивных сетевых интерфейсов.
Args:
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
координат (x, y) для интерфейсов. По умолчанию None.
Returns:
List[NetworkInterface]: Список интерфейсов с state != "100".
"""
all_interfaces = await self.get_network_interfaces(initial_positions)
return [
interface for interface in all_interfaces
@ -255,9 +337,18 @@ class NetworkService:
container_x: int = 0,
container_y: int = 0,
) -> NetworkInterfacesContainer:
"""Получает объект, содержащий список активных интерфейсов и координаты контейнера"""
return await self.get_interfaces_container("100", initial_positions,
container_x, container_y)
"""Получает объект, содержащий список активных интерфейсов и координаты контейнера.
Args:
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
координат (x, y) для интерфейсов. По умолчанию None.
container_x (int, optional): Координата X контейнера. По умолчанию 0.
container_y (int, optional): Координата Y контейнера. По умолчанию 0.
Returns:
NetworkInterfacesContainer: Объект с активными интерфейсами и координатами.
"""
return await self.get_interfaces_container("100", initial_positions, container_x, container_y)
async def get_deactivated_interfaces_container(
self,
@ -265,8 +356,16 @@ class NetworkService:
container_x: int = 0,
container_y: int = 0,
) -> NetworkInterfacesContainer:
"""Получает объект, содержащий список неактивных интерфейсов и координаты контейнера"""
"""Получает объект, содержащий список неактивных интерфейсов и координаты контейнера.
Args:
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
координат (x, y) для интерфейсов. По умолчанию None.
container_x (int, optional): Координата X контейнера. По умолчанию 0.
container_y (int, optional): Координата Y контейнера. По умолчанию 0.
Returns:
NetworkInterfacesContainer: Объект с неактивными интерфейсами и координатами.
"""
interfaces = await self.get_deactivated_interfaces(initial_positions)
return NetworkInterfacesContainer(interfaces=interfaces,
container_x=container_x,
container_y=container_y)
return NetworkInterfacesContainer(interfaces=interfaces, container_x=container_x, container_y=container_y)

View file

@ -11,7 +11,6 @@ from functools import lru_cache
import json
from pathlib import Path
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import subprocess
from models.subnet import SubnetScanResult, Device
@ -26,11 +25,33 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
class NetworkScanner:
"""Класс для сканирования подсети с использованием ARP-запросов.
Выполняет обнаружение устройств в указанной подсети через ARP-пакеты.
"""
def __init__(self, interface: str):
"""Инициализирует сканер сети с указанным сетевым интерфейсом.
Args:
interface (str): Название сетевого интерфейса (например, "eth0").
"""
self.interface = interface
conf.iface = interface
def scan(self, subnet: str) -> List[Tuple[str, str]]:
"""Сканирует подсеть с использованием ARP-запросов.
Args:
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
Returns:
List[Tuple[str, str]]: Список кортежей (IP-адрес, MAC-адрес) обнаруженных
устройств.
Raises:
Exception: Если произошла ошибка при отправке ARP-пакетов.
"""
try:
arp = ARP(pdst=subnet)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
@ -46,13 +67,42 @@ class NetworkScanner:
return []
class PortScanner:
"""Класс для сканирования открытых портов на устройствах.
Выполняет сканирование портов с использованием TCP SYN-запросов.
"""
def __init__(self, interface: str):
"""Инициализирует сканер портов с указанным сетевым интерфейсом.
Args:
interface (str): Название сетевого интерфейса (например, "eth0").
"""
self.interface = interface
def scan(self, ip: str) -> List[str]:
"""Сканирует открытые порты на указанном IP-адресе.
Args:
ip (str): IP-адрес устройства для сканирования.
Returns:
List[str]: Список имён открытых портов (например, "HTTP", "SSH").
Raises:
Exception: Если произошла ошибка при сканировании портов.
"""
open_ports = []
def check_port(port: int) -> Optional[str]:
"""Проверяет доступность конкретного порта.
Args:
port (int): Номер порта для проверки.
Returns:
Optional[str]: Имя порта, если открыт, иначе None.
"""
try:
pkt = sr1(
IP(dst=ip) / TCP(dport=port, flags="S"),
@ -80,13 +130,33 @@ class PortScanner:
return []
class DeviceInfoCollector:
"""Класс для сбора информации об устройствах, включая вендоров и хостнеймы.
Управляет кэшированием данных NetBIOS и DNS.
"""
def __init__(self):
"""Инициализирует коллектор информации об устройствах.
Создаёт объект MacLookup для определения вендоров и инициализирует кэши.
"""
self.mac_lookup = MacLookup()
self.nbtscan_cache = {}
self.nbtscan_results = {}
@lru_cache(maxsize=1000)
def get_vendor(self, mac: str) -> str:
"""Определяет вендора по MAC-адресу с использованием кэширования.
Args:
mac (str): MAC-адрес устройства в формате XX:XX:XX:XX:XX:XX.
Returns:
str: Название вендора или "Unknown", если вендор не определён.
Raises:
Exception: Если произошла ошибка при запросе к базе вендоров.
"""
try:
return self.mac_lookup.lookup(mac)
except Exception as e:
@ -95,6 +165,13 @@ class DeviceInfoCollector:
return "Unknown"
def scan_nbtscan(self, subnet: str) -> None:
"""Выполняет сканирование NetBIOS для указанной подсети.
Args:
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
Сохраняет результаты в кэш, игнорируя хостнеймы "<unknown>".
"""
if subnet not in self.nbtscan_results:
try:
logger.debug(f"Запуск nbtscan для подсети {subnet}")
@ -121,6 +198,16 @@ class DeviceInfoCollector:
logger.error(f"NetBIOS: Ошибка для подсети {subnet}: {str(e)}")
def get_hostname(self, ip: str) -> Optional[str]:
"""Определяет хостнейм для указанного IP-адреса.
Args:
ip (str): IP-адрес устройства.
Returns:
Optional[str]: Хостнейм, если найден, иначе None.
Сначала пытается использовать DNS, затем NetBIOS через nbtscan.
"""
try:
socket.setdefaulttimeout(2)
return socket.gethostbyaddr(ip)[0]
@ -138,12 +225,33 @@ class DeviceInfoCollector:
return None
class DeviceStorage:
"""Класс для хранения и управления данными об устройствах.
Хранит устройства в JSON-файле и удаляет устаревшие записи.
"""
def __init__(self, storage_path: str = "etc/gonets/SRV/storage/devices/devices.json"):
"""Инициализирует хранилище устройств с указанным путём.
Args:
storage_path (str, optional): Путь к файлу хранения.
По умолчанию "etc/gonets/SRV/storage/devices/devices.json".
Создаёт родительские директории, если они не существуют.
"""
self.storage_file = Path(storage_path)
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
self.offline_timeout = timedelta(days=7)
def load_devices(self) -> Dict[str, Device]:
"""Загружает устройства из файла хранения.
Returns:
Dict[str, Device]: Словарь устройств, где ключ — MAC-адрес.
Удаляет устройства с неверным форматом last_seen и возвращает пустой словарь
при ошибке.
"""
try:
if self.storage_file.exists():
with open(self.storage_file, "r", encoding="utf-8") as f:
@ -160,6 +268,15 @@ class DeviceStorage:
return {}
def _clean_old_devices(self, devices: Dict[str, Device]) -> Dict[str, Device]:
"""Очищает устаревшие устройства из словаря.
Args:
devices (Dict[str, Device]): Словарь устройств с MAC-адресами как ключами.
Returns:
Dict[str, Device]: Отфильтрованный словарь, исключающий устройства
offline более 7 дней.
"""
now = datetime.now()
filtered = {}
for mac, device in devices.items():
@ -177,6 +294,14 @@ class DeviceStorage:
return filtered
def save_devices(self, devices: List[Device]) -> None:
"""Сохраняет список устройств в файл.
Args:
devices (List[Device]): Список объектов Device для сохранения.
Raises:
Exception: Если произошла ошибка при записи в файл.
"""
try:
data = SubnetScanResult(devices=devices).model_dump()
with open(self.storage_file, "w", encoding="utf-8") as f:
@ -186,7 +311,16 @@ class DeviceStorage:
logger.error(f"Ошибка при сохранении устройств: {str(e)}")
class SubnetService:
"""Сервис для сканирования подсетей и управления устройствами.
Объединяет функционал сканирования сети, портов и хранения данных об устройствах.
"""
def __init__(self):
"""Инициализирует сервис подсети с настройкой всех компонентов.
Автоматически определяет интерфейс и инициализирует сканеры и хранилище.
"""
self.interface = self._get_default_interface()
self.network_scanner = NetworkScanner(self.interface)
self.port_scanner = PortScanner(self.interface)
@ -194,6 +328,13 @@ class SubnetService:
self.storage = DeviceStorage()
def _get_default_interface(self) -> str:
"""Определяет имя сетевого интерфейса по умолчанию.
Returns:
str: Название интерфейса (например, "eth0").
Использует информацию о шлюзе или первый доступный интерфейс, отличный от "lo".
"""
try:
gateways = netifaces.gateways()
default_gateway = gateways.get("default", {}).get(netifaces.AF_INET)
@ -209,6 +350,19 @@ class SubnetService:
return "eth0"
def scan_subnet(self, subnet: str, ports: List[int] = None) -> SubnetScanResult:
"""Сканирует указанную подсеть и возвращает результаты.
Args:
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
ports (List[int], optional): Список портов для сканирования.
По умолчанию None (используются порты из config.ports).
Returns:
SubnetScanResult: Объект с результатами сканирования, содержащий список
устройств.
Сохраняет данные в хранилище и обновляет статус устройств.
"""
start_time = time.time()
devices = []
now = datetime.now().astimezone()
@ -228,6 +382,14 @@ class SubnetService:
self.device_info.scan_nbtscan(subnet)
def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]:
"""Обрабатывает устройство из пары IP и MAC.
Args:
ip_mac (Tuple[str, str]): Кортеж с IP-адресом и MAC-адресом.
Returns:
Optional[Device]: Объект Device или None, если устройство локальное.
"""
ip, mac = ip_mac
if local_device and ip == local_device.ip:
return None
@ -286,6 +448,14 @@ class SubnetService:
return SubnetScanResult(devices=devices)
def _get_local_device(self, subnet: str) -> Optional[Device]:
"""Определяет локальное устройство в указанной подсети.
Args:
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
Returns:
Optional[Device]: Объект локального устройства или None, если не найдено.
"""
now = datetime.now().astimezone()
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
try:

View file

@ -6,11 +6,9 @@ from functools import lru_cache
from typing import List, Optional, Dict, Tuple
from mac_vendor_lookup import MacLookup
import pywifi
from pywifi import Profile
from models.wifi import WifiAccessPoint, WifiScanResult
from pathlib import Path
import json
from datetime import datetime
logging.basicConfig(
level=logging.DEBUG,
@ -18,14 +16,34 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
class WifiStorage:
"""Класс для хранения данных о Wi-Fi сетях и их заметках по SSID."""
"""Класс для хранения данных о Wi-Fi сетях и их заметках по SSID.
Предоставляет методы для загрузки и сохранения заметок, связанных с SSID сетей,
в JSON-файле на диске.
"""
def __init__(self, storage_path: str = "etc/gonets/SRV/storage/wifi/wifi.json"):
"""Инициализирует объект хранилища Wi-Fi с указанным путём к файлу.
Args:
storage_path (str, optional): Путь к файлу для хранения заметок.
По умолчанию "etc/gonets/SRV/storage/wifi/wifi.json".
Создаёт родительские директории, если они не существуют.
"""
self.storage_file = Path(storage_path)
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
def load_notes(self) -> Dict[str, Optional[str]]:
"""Загружает заметки по SSID из файла хранения."""
"""Загружает заметки по SSID из файла хранения.
Returns:
Dict[str, Optional[str]]: Словарь, где ключ — SSID, значение — заметка
или None, если заметки нет.
Raises:
Exception: Если произошла ошибка при чтении или парсинге файла.
"""
try:
if self.storage_file.exists():
with open(self.storage_file, "r", encoding="utf-8") as f:
@ -37,7 +55,15 @@ class WifiStorage:
return {}
def save_notes(self, notes: Dict[str, Optional[str]]) -> None:
"""Сохраняет заметки по SSID в файл."""
"""Сохраняет заметки по SSID в файл.
Args:
notes (Dict[str, Optional[str]]): Словарь с SSID как ключами и заметками
как значениями.
Raises:
Exception: Если произошла ошибка при записи в файл.
"""
try:
data = {"networks": [{"ssid": ssid, "note": note} for ssid, note in notes.items()]}
with open(self.storage_file, "w", encoding="utf-8") as f:
@ -47,16 +73,33 @@ class WifiStorage:
logger.error(f"Ошибка при сохранении заметок: {str(e)}")
class WifiService:
"""Сервис для сканирования текущих Wi-Fi точек доступа"""
"""Сервис для сканирования текущих Wi-Fi точек доступа.
Предоставляет функционал для обнаружения Wi-Fi сетей с использованием pywifi и iw,
определения характеристик точек доступа (сигнал, шифрование, канал и т.д.),
а также управления заметками.
"""
def __init__(self):
"""Инициализирует сервис Wi-Fi с настройкой интерфейса и хранилища.
Инициализирует объект MacLookup для определения вендоров по MAC-адресам,
PyWiFi для работы с Wi-Fi-интерфейсами и объект хранилища заметок.
"""
self.mac_lookup = MacLookup()
self.wifi = pywifi.PyWiFi()
self.interface = self._get_wifi_interface()
self.storage = WifiStorage()
def _get_wifi_interface(self) -> Optional[object]:
"""Автоматически определяет активный Wi-Fi-интерфейс"""
"""Автоматически определяет активный Wi-Fi-интерфейс.
Returns:
Optional[object]: Объект интерфейса PyWiFi или None, если интерфейс не найден.
Проверяет доступные интерфейсы, выполняет тестовое сканирование и возвращает
первый активный интерфейс, исключая P2P-интерфейсы.
"""
try:
interfaces = self.wifi.interfaces()
if not interfaces:
@ -93,6 +136,17 @@ class WifiService:
@lru_cache(maxsize=1000)
def get_vendor(self, mac: str) -> str:
"""Определяет вендора по MAC-адресу с использованием кэширования.
Args:
mac (str): MAC-адрес устройства в формате XX:XX:XX:XX:XX:XX.
Returns:
str: Название вендора или "Unknown", если вендор не определён.
Raises:
Exception: Если произошла ошибка при запросе к базе вендоров.
"""
try:
return self.mac_lookup.lookup(mac)
except Exception as e:
@ -101,7 +155,14 @@ class WifiService:
return "Unknown"
def _get_encryption_from_iw(self, ap_info: Dict) -> str:
"""Определяет шифрование из данных iw"""
"""Определяет тип шифрования из данных, полученных через iw.
Args:
ap_info (Dict): Словарь с данными точки доступа, включая поле "security".
Returns:
str: Тип шифрования ("WPA3", "WPA2", "WPA", "WEP" или "Open").
"""
security = ap_info.get("security", "")
logger.debug(
f"Шифрование для BSSID {ap_info.get('bssid')}: {security}")
@ -116,7 +177,14 @@ class WifiService:
return "Open"
def _get_channel(self, frequency: Optional[int]) -> Optional[int]:
"""Преобразует частоту в канал."""
"""Преобразует частоту в номер канала.
Args:
frequency (Optional[int]): Частота в МГц или None.
Returns:
Optional[int]: Номер канала или None, если частота вне диапазона.
"""
if frequency is None:
return None
try:
@ -133,7 +201,14 @@ class WifiService:
return None
def _get_frequency(self, frequency: Optional[int]) -> Optional[str]:
"""Определяет частотный диапазон."""
"""Определяет частотный диапазон на основе частоты.
Args:
frequency (Optional[int]): Частота в МГц или None.
Returns:
Optional[str]: Диапазон ("2.4 GHz", "5 GHz" или None).
"""
if frequency is None:
return None
try:
@ -150,7 +225,17 @@ class WifiService:
return None
def _get_iw_scan_data(self) -> List[Dict]:
"""Получает данные сканирования через iw."""
"""Получает данные сканирования Wi-Fi через команду iw.
Returns:
List[Dict]: Список словарей с данными точек доступа (BSSID, SSID, частота,
сигнал, шифрование).
Raises:
subprocess.CalledProcessError: Если команда iw завершилась с ошибкой.
subprocess.TimeoutExpired: Если превышен таймаут выполнения.
Exception: Если произошла ошибка при парсинге вывода.
"""
try:
if not self.interface:
logger.error("Интерфейс не определён")
@ -225,7 +310,18 @@ class WifiService:
return []
def scan_wifi(self, timeout: int = 30) -> WifiScanResult:
"""Сканирует текущие Wi-Fi точки доступа"""
"""Сканирует текущие Wi-Fi точки доступа.
Args:
timeout (int, optional): Таймаут ожидания в секундах. По умолчанию 30.
Returns:
WifiScanResult: Объект с результатами сканирования, содержащий список точек
доступа.
Сканирует Wi-Fi с использованием pywifi и iw, объединяет данные, определяет
лучшие точки по сигналу и сохраняет заметки.
"""
start_time = time.time()
aps = []
@ -282,12 +378,10 @@ class WifiService:
if ssid not in best_aps_by_ssid or (
current_ap.signal and best_aps_by_ssid[ssid].signal
and current_ap.signal > best_aps_by_ssid[ssid].signal):
# При обновлении или добавлении точки сохраняем заметку из saved_notes
current_ap.note = saved_notes.get(ssid)
best_aps_by_ssid[ssid] = current_ap
aps = list(best_aps_by_ssid.values())
# Сохраняем заметки для всех найденных SSID
notes_to_save = {ap.ssid: ap.note for ap in aps if ap.ssid}
self.storage.save_notes(notes_to_save)
@ -313,7 +407,7 @@ class WifiService:
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
Exception: Если произошла ошибка при сохранении заметки.
"""
try:
saved_notes = self.storage.load_notes()
@ -343,7 +437,7 @@ class WifiService:
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
Exception: Если произошла ошибка при сохранении заметки.
"""
try:
saved_notes = self.storage.load_notes()
@ -373,7 +467,7 @@ class WifiService:
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
Exception: Если произошла ошибка при сохранении изменений.
"""
try:
saved_notes = self.storage.load_notes()