Инициализация в новом репозитории

This commit is contained in:
commit eb1f18f53e
374 changed files with 226678 additions and 0 deletions

0
SRV/services/__init__.py Normal file
View file

View file

@ -0,0 +1,59 @@
from scapy.layers.l2 import ARP, Ether
from scapy.sendrecv import srp
import netifaces
from netaddr import IPNetwork
class NetworkScanner:
"""Иницилазицая сетевого сканера"""
def __init__(self, interface=None):
"""Инициализация параметров интерфейсов """
self.interface = interface or netifaces.gateways()['default'][netifaces.AF_INET][1]
self.ip_info = netifaces.ifaddresses(self.interface).get(netifaces.AF_INET, [])
self.networks = [IPNetwork(f"{ip['addr']}/{ip['netmask']}") for ip in self.ip_info]
self.local_mac = netifaces.ifaddresses(self.interface)[netifaces.AF_LINK][0]['addr']
def _create_arp_packet(self, target_ip):
"""Создание пакета ARP для отправки"""
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):
"""Подготовка к сканированию сети"""
all_clients = []
for network in self.networks:
target_ip = f"{network.network}/{network.prefixlen}"
packet = self._create_arp_packet(target_ip)
clients = []
for _ in range(attempts):
result = srp(packet, timeout=timeout, iface=self.interface, verbose=0)[0]
for _, received in result:
client = {'ip': received.psrc, 'mac': received.hwsrc}
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):
"""Лист адресов клиентов"""
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)

View file

@ -0,0 +1,129 @@
# services/auth_service.py
from jose import JWTError, jwt
from datetime import datetime, timedelta
from fastapi import HTTPException, Depends, Request
from fastapi.security import OAuth2PasswordBearer
from models.auth import UserCreate, UserLogin, UserResponse, Token
from storage.json_storage import JsonStorage
from storage.session_storage import SessionStorage
from typing import Optional
import configparser
import os
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
config = configparser.ConfigParser()
config_path = "/etc/gonets/config.ini"
if not os.path.exists(config_path):
raise RuntimeError(f"Конфигурационный файл {config_path} не найден")
config.read(config_path)
SECRET_KEY = config["JWT"]["secret_key"]
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
class AuthService:
"""Сервис для регистрации, авторизации и управления сессиями"""
def __init__(self):
self.storage = JsonStorage()
self.session_storage = SessionStorage()
def create_access_token(self, data: dict) -> str:
"""Создаёт токен"""
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_token(self, token: str) -> Optional[dict]:
"""Проверяет токен"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
return None
return {"username": username}
except JWTError:
return None
def register(self, user: UserCreate) -> UserResponse:
"""Регистрирует нового пользователя"""
try:
db_user = self.storage.create_user(user)
return db_user
except ValueError as e:
logger.error(f"Ошибка при регистрации пользователя {user.username}: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Ошибка при регистрации пользователя {user.username}: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Ошибка регистрации")
def login(self, user: UserLogin, client_id: str) -> dict:
"""Авторизует пользователя, создаёт сессию и возвращает токен"""
try:
db_user = self.storage.get_user_by_username(user.username)
if not db_user:
logger.error(f"Пользователь {user.username} не найден")
raise HTTPException(status_code=401, detail="Неверный логин или пароль")
if not self.storage.verify_password(user.password, db_user["password_hash"]):
logger.error(f"Неверный пароль для пользователя {user.username}")
raise HTTPException(status_code=401, detail="Неверный логин или пароль")
access_token = self.create_access_token(data={"sub": user.username})
session_id = self.session_storage.create_session(user.username, access_token, client_id)
logger.info(f"Пользователь {user.username} успешно авторизован")
return {
"access_token": access_token,
"token_type": "bearer",
"session_id": session_id,
"user": UserResponse(id=db_user["id"], username=db_user["username"], is_admin=db_user["is_admin"])
}
except Exception as e:
logger.error(f"Ошибка при авторизации пользователя {user.username}: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Ошибка авторизации")
def get_current_user(self, token: str = Depends(oauth2_scheme)) -> UserResponse:
"""Получает текущего пользователя по токену"""
credentials = self.verify_token(token)
if not credentials:
logger.error("Неверный или истёкший токен")
raise HTTPException(status_code=401, detail="Неверный токен")
username = credentials["username"]
db_user = self.storage.get_user_by_username(username)
if not db_user:
logger.error(f"Пользователь {username} не найден")
raise HTTPException(status_code=401, detail="Пользователь не найден")
return UserResponse(id=db_user["id"], username=db_user["username"], is_admin=db_user["is_admin"])
def get_user_by_session(self, client_id: str) -> dict:
"""Получает пользователя и токен по client_id"""
session = self.session_storage.get_latest_session(client_id)
if not session:
logger.error(f"Сессия для client_id {client_id} не найдена или истекла")
raise HTTPException(status_code=401, detail="Сессия недействительна")
username = session["username"]
token = session["token"]
db_user = self.storage.get_user_by_username(username)
if not db_user:
logger.error(f"Пользователь {username} не найден")
raise HTTPException(status_code=401, detail="Пользователь не найден")
return {
"user": UserResponse(id=db_user["id"], username=db_user["username"], is_admin=db_user["is_admin"]),
"access_token": token
}
def logout(self, client_id: str):
"""Выполняет выход из системы, удаляя последнюю сессию для client_id"""
try:
session = self.session_storage.get_latest_session(client_id)
if session:
self.session_storage.delete_session(session["session_id"])
logger.info(f"Пользователь успешно вышел из системы для client_id {client_id}")
return {"message": "Успешный выход из системы"}
except Exception as e:
logger.error(f"Ошибка при выходе из системы: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Ошибка при выходе из системы")

View file

@ -0,0 +1,143 @@
from re import DEBUG
from typing import Dict, List, Optional
from pydbus import SystemBus
DEBUG = True
class FirewalldService:
def __init__(self):
self.bus = SystemBus()
self.proxy = self.bus.get(
"org.fedoraproject.FirewallD1", "/org/fedoraproject/FirewallD1"
)
def get_zones(self) -> List[str]:
"""Get list of all zones."""
try:
return self.proxy.getZones()
except Exception as e:
raise Exception(f"Failed to get zones: {str(e)}")
def get_default_zone(self) -> str:
"""Get the default zone."""
try:
return self.proxy.getDefaultZone()
except Exception as e:
raise Exception(f"Failed to get default zone: {str(e)}")
def set_default_zone(self, zone: str) -> None:
"""Set the default zone."""
try:
self.proxy.setDefaultZone(zone)
except Exception as e:
raise Exception(f"Failed to set default zone to {zone}: {str(e)}")
def add_service(self, zone: str, service: str, timeout: Optional[int] = 0) -> None:
"""Add a service to a zone."""
try:
self.proxy.addService(zone, service, timeout)
except Exception as e:
raise Exception(f"Failed to add service {service} to zone {zone}: {str(e)}")
def remove_service(self, zone: str, service: str) -> None:
"""Remove a service from a zone."""
try:
self.proxy.removeService(zone, service)
except Exception as e:
raise Exception(
f"Failed to remove service {service} from zone {zone}: {str(e)}"
)
def get_services(self, zone: str) -> List[str]:
"""Get list of services in a zone."""
try:
return self.proxy.getServices(zone)
except Exception as e:
raise Exception(f"Failed to get services for zone {zone}: {str(e)}")
def add_port(
self, zone: str, port: int, protocol: str, timeout: Optional[int] = 0
) -> None:
"""Add a port to a zone."""
try:
if DEBUG:
print(f"Adding port {port}/{protocol} to zone {zone}")
print(f"Timeout: {timeout}")
# Get the zone interface
zone_proxy = self.bus.get(
"org.fedoraproject.FirewallD1",
"/org/fedoraproject/FirewallD1"
)
zone_iface = zone_proxy['org.fedoraproject.FirewallD1.zone']
# Convert port to string
port_str = str(port)
# Convert timeout to integer
timeout_int = int(timeout) if timeout is not None else 0
# Call addPort with the correct signature (sssi)
zone_iface.addPort(zone, port_str, protocol, timeout_int)
except Exception as e:
raise Exception(
f"Failed to add port {port}/{protocol} to zone {zone}: {str(e)}"
)
def remove_port(self, zone: str, port: int, protocol: str) -> None:
"""Remove a port from a zone."""
try:
port_str = f"{port}/{protocol}"
self.proxy.removePort(zone, port_str)
except Exception as e:
raise Exception(
f"Failed to remove port {port}/{protocol} from zone {zone}: {str(e)}"
)
def get_ports(self, zone: str) -> List[str]:
"""Get list of ports in a zone."""
try:
ports = self.proxy.getPorts(zone)
# Convert port list items to strings in format "port/protocol"
return [f"{port[0]}/{port[1]}" for port in ports]
except Exception as e:
raise Exception(f"Failed to get ports for zone {zone}: {str(e)}")
def add_source(self, zone: str, source: str) -> None:
"""Add a source to a zone."""
try:
self.proxy.addSource(zone, source)
except Exception as e:
raise Exception(f"Failed to add source {source} to zone {zone}: {str(e)}")
def remove_source(self, zone: str, source: str) -> None:
"""Remove a source from a zone."""
try:
self.proxy.removeSource(zone, source)
except Exception as e:
raise Exception(
f"Failed to remove source {source} from zone {zone}: {str(e)}"
)
def get_sources(self, zone: str) -> List[str]:
"""Get list of sources in a zone."""
try:
return self.proxy.getSources(zone)
except Exception as e:
raise Exception(f"Failed to get sources for zone {zone}: {str(e)}")
def reload(self) -> None:
"""Reload firewall rules."""
try:
self.proxy.reload()
except Exception as e:
raise Exception(f"Failed to reload firewall rules: {str(e)}")
def get_zone_settings(self, zone: str) -> Dict:
"""Get all settings for a zone."""
try:
return {
"services": self.get_services(zone),
"ports": self.get_ports(zone),
"sources": self.get_sources(zone),
}
except Exception as e:
raise Exception(f"Failed to get zone settings for {zone}: {str(e)}")

View file

@ -0,0 +1,75 @@
import asyncio
from typing import Dict, Any
from sdbus_async.networkmanager import (
NetworkManager,
NetworkManagerSettings,
NetworkConnectionSettings,
NetworkDeviceGeneric,
)
import sdbus
from sdbus_async.networkmanager.settings import ConnectionProfile
from services.arp_service import NetworkScanner
class IPService:
"""
Сервис проверки доступности IP-адреса и применения настроек сети
"""
def __init__(self):
self.arp_scanner = NetworkScanner()
self.bus = sdbus.sd_bus_open_system()
sdbus.set_default_bus(self.bus)
def _reset_bus(self):
""" Перезапускает соединение с системной шиной 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:
self.bus = None
self.bus = new_bus
async def check_ip_config(self, args: Dict[str, Any]) -> Dict[str, Any]:
""" Проверяет доступность IP-адреса перед его применением """
self._reset_bus()
clients = self.arp_scanner.scan_network()
new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"]
if any(client['ip'] == new_ip for client in clients):
return {"available": False, "message": f"IP address {new_ip} уже занят."}
return {"available": True, "message": "Network profile settings are available for use."}
async def apply_ip_config(self, args: Dict[str, Any]) -> None:
""" Применяет проверенные настройки IP """
self._reset_bus()
nm = NetworkManager()
settings = NetworkManagerSettings()
connection_paths = await settings.get_connections_by_id(args["connection_id"])
if not connection_paths:
raise ValueError(f"Подключение {args['connection_id']} не найдено")
connection = NetworkConnectionSettings(connection_paths[0])
profile = await connection.get_profile()
settings_dict = profile.to_settings_dict()
for key_path, value in args["ipv4_updates"].items():
section, *props = key_path.split('.')
current = settings_dict[section]
for prop in props[:-1]:
current = current.setdefault(prop, {})
current[props[-1]] = value
new_profile = ConnectionProfile.from_settings_dict(settings_dict)
await connection.update(new_profile.to_dbus())
print(f"Обновлены настройки для {args['connection_id']}:")
interface_name = settings_dict['connection']['interface-name']
devices = await nm.get_devices()
for device_path in devices:
device = NetworkDeviceGeneric(device_path)
if await device.interface == interface_name:
await device.disconnect()
print(f"Отключён от {interface_name}")
await nm.activate_connection(connection=connection_paths[0], device=device_path)
print(f"Переподключён к {args['connection_id']}")
return
raise RuntimeError(f"Устройство для интерфейса {interface_name} не найдено")

View file

@ -0,0 +1,200 @@
from typing import List, Dict, Tuple, Union, Optional
from sdbus_async.networkmanager import (
NetworkManager,
NetworkDeviceGeneric,
IPv4Config,
IPv6Config,
NetworkManagerSettings,
NetworkDeviceWireless,
AccessPoint,
DeviceType
)
from sdbus import sd_bus_open_system
from models.network import (
NetworkInterface,
IPv4Route,
IPv6Route,
IPConfig,
IndividualIPConfig,
NetworkInterfacesContainer,
WiFiDevice
)
import subprocess
class NetworkService:
"""Сервис для взаимодействия с NetworkManager через D-Bus"""
def __init__(self):
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 для устройства"""
try:
config = await device.get_applied_connection()
settings = config[0].get(ip_type, {})
return str(settings.get("method", ('s', 'unknown'))[1])
except Exception:
return 'unknown'
async def get_connection_name(self, device: NetworkDeviceGeneric) -> str:
"""Извлекает имя соединения (GENERAL.CONNECTION) для устройства"""
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 для устройства"""
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)
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],
prefix=route.get('prefix', ('u', 0))[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}'),
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')]
)
configs.append(config)
return configs
async def get_wifi_info(self, wifi_device: NetworkDeviceWireless) -> Dict[str, Optional[Union[str, int]]]:
"""Получает информацию о Wi-Fi устройстве"""
active_ap = await wifi_device.active_access_point
wifi_info = {
'ssid': None,
'bssid': None,
'bitrate': await wifi_device.bitrate,
'strength': None,
'frequency': None,
'mode': None
}
if active_ap != '/':
ap = AccessPoint(active_ap, self.system_bus)
wifi_info.update({
'ssid': await ap.ssid,
'bssid': await ap.hw_address,
'strength': await ap.strength,
'frequency': await ap.frequency,
'mode': await ap.mode
})
return wifi_info
async def get_network_interfaces(self, initial_positions: Dict[str, Tuple[int, int]] = None) -> List[Union[NetworkInterface, WiFiDevice]]:
"""Получает список сетевых интерфейсов"""
devices_paths = await self.nm.get_devices()
network_interfaces = []
initial_positions = initial_positions or {}
for device_path in devices_paths:
generic_device = NetworkDeviceGeneric(device_path, self.system_bus)
interface_name = str(await generic_device.interface)
device_type = DeviceType(await generic_device.device_type).name
if device_type == 'LOOPBACK':
continue
device_state = str(await generic_device.state)
if device_state == '10':
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],
}
if device_type == "WIFI":
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))
else:
network_interfaces.append(NetworkInterface(**common_data))
return network_interfaces
async def get_hostname(self) -> Dict[str, str]:
"""Возвращает имя хоста из команды hostname в формате JSON"""
try:
result = subprocess.run(['hostname'], capture_output=True, text=True, check=True)
hostname = result.stdout.strip()
return {"hostname": hostname}
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Failed to execute hostname: {e}")
except Exception as e:
raise RuntimeError(f"Error processing hostname output: {e}")
async def get_interfaces_by_state(self, state: str, initial_positions: Dict[str, Tuple[int, int]] = None) -> List[NetworkInterface]:
"""
Получает список интерфейсов по состоянию
"""
all_interfaces = await self.get_network_interfaces(initial_positions)
return [interface for interface in all_interfaces if interface.device_state == state]
async def get_interfaces_container(self, state: Optional[str] = None, initial_positions: Dict[str, Tuple[int, int]] = None, container_x: int = 0, container_y: int = 0) -> NetworkInterfacesContainer:
""" Получает объект, содержащий список интерфейсов и координаты контейнера """
if state:
interfaces = await self.get_interfaces_by_state(state, initial_positions)
else:
interfaces = await self.get_network_interfaces(initial_positions)
return NetworkInterfacesContainer(
interfaces=interfaces,
container_x=container_x,
container_y=container_y
)
async def get_network_interfaces_container(self, initial_positions: Optional[Dict[str, Tuple[int, int]]] = None, ) -> NetworkInterfacesContainer:
""" Получает контейнер с сетевыми интерфейсами и координатами контейнера """
interfaces = await self.get_network_interfaces(initial_positions)
return NetworkInterfacesContainer(
interfaces=interfaces,
container_x=0,
container_y=0
)
async def get_activated_interfaces(self, initial_positions: Dict[str, Tuple[int, int]] = None) -> List[NetworkInterface]:
""" Получает список активных сетевых интерфейсов """
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]:
""" Получает список неактивных сетевых интерфейсов """
all_interfaces = await self.get_network_interfaces(initial_positions)
return [interface for interface in all_interfaces if interface.device_state != '100']
async def get_activated_interfaces_container(self, initial_positions: Dict[str, Tuple[int, int]] = None, container_x: int = 0, container_y: int = 0) -> NetworkInterfacesContainer:
""" Получает объект, содержащий список активных интерфейсов и координаты контейнера """
return await self.get_interfaces_container('100', initial_positions, container_x, container_y)
async def get_deactivated_interfaces_container(self, initial_positions: Dict[str, Tuple[int, int]] = None, container_x: int = 0, container_y: int = 0) -> NetworkInterfacesContainer:
""" Получает объект, содержащий список неактивных интерфейсов и координаты контейнера """
interfaces = await self.get_deactivated_interfaces(initial_positions)
return NetworkInterfacesContainer(
interfaces=interfaces,
container_x=container_x,
container_y=container_y
)

View file

@ -0,0 +1,317 @@
from scapy.config import conf
from manuf import MacParser
from typing import List, Optional, Tuple, Dict
import socket
import logging
import netifaces
import ipaddress
from concurrent.futures import ThreadPoolExecutor
import time
from functools import lru_cache
import json
from pathlib import Path
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import os
from models.subnet import SubnetScanResult, Device
from config.ports import port_config
from scapy.layers.inet import IP, TCP
from scapy.layers.l2 import ARP, Ether
from scapy.sendrecv import srp, sr1
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
# Подавляем логи Scapy
logging.getLogger("scapy").setLevel(logging.ERROR)
def normalize_mac(mac: str) -> str:
"""Нормализует MAC-адрес к формату XX:XX:XX:XX:XX:XX."""
if not mac:
return ""
mac = mac.replace(":", "").replace("-", "").upper()
return ":".join(mac[i:i + 2] for i in range(0, 12, 2))
class NetworkScanner:
"""Класс для сканирования сети с помощью ARP-запросов."""
def __init__(self, interface: str):
self.interface = interface
conf.iface = interface
self.arp_cache: Dict[str, str] = {}
def scan(self, subnet: str) -> List[Tuple[str, str]]:
try:
arp = ARP(pdst=subnet)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
packet = ether / arp
result = srp(packet, timeout=5, verbose=False, iface=self.interface)[0]
devices = [(received.psrc, normalize_mac(received.hwsrc)) for sent, received in result]
for ip, mac in devices:
self.arp_cache[ip] = mac
logger.debug(f"ARP-кэш обновлён: {len(self.arp_cache)} записей")
return devices
except Exception as e:
logger.error(f"Ошибка ARP-сканирования подсети {subnet}: {str(e)}")
return []
class PortScanner:
"""Класс для сканирования портов на устройстве."""
def __init__(self, interface: str, arp_cache: Dict[str, str]):
self.interface = interface
self.arp_cache = arp_cache
def scan(self, ip: str) -> List[str]:
open_ports = []
def check_port(port: int) -> Optional[str]:
try:
dst_mac = self.arp_cache.get(ip, "ff:ff:ff:ff:ff:ff")
pkt = sr1(
Ether(dst=dst_mac) / IP(dst=ip) / TCP(dport=port, flags="S"),
timeout=2,
verbose=False,
iface=self.interface
)
if pkt and pkt.haslayer(TCP) and pkt[TCP].flags == 0x12:
return port_config.get_port_name(port)
return None
except Exception as e:
logger.debug(f"Ошибка проверки порта {port} для IP {ip}: {str(e)}")
return None
try:
max_workers = min(len(port_config.get_ports()), 20) # Уменьшено до 20
with ThreadPoolExecutor(max_workers=max_workers) as executor:
results = executor.map(check_port, port_config.get_ports())
open_ports = [port for port in results if port is not None]
logger.debug(f"Открытые порты для IP {ip}: {open_ports}")
return open_ports
except Exception as e:
logger.warning(f"Ошибка при сканировании портов для IP {ip}: {str(e)}")
return []
class DeviceInfoCollector:
"""Класс для сбора информации об устройстве."""
def __init__(self):
self.mac_lookup = MacParser()
try:
logger.info("Обновление базы производителей MAC...")
self.mac_lookup.update()
logger.info("База производителей обновлена")
except Exception as e:
logger.warning(f"Не удалось обновить базу производителей: {str(e)}")
# Проверяем наличие локальной базы
manuf_db = Path(os.path.expanduser("~/.cache/manuf/manuf.txt"))
if manuf_db.exists():
logger.info(f"Используется локальная база OUI: {manuf_db}")
else:
logger.error("Локальная база OUI отсутствует, vendors могут быть Unknown")
@lru_cache(maxsize=1000)
def get_vendor(self, mac: str) -> str:
if not mac or mac.lower() == "ff:ff:ff:ff:ff:ff":
return "Unknown"
normalized_mac = normalize_mac(mac)
try:
vendor = self.mac_lookup.get_manuf(normalized_mac)
if vendor is None:
vendor = "Unknown"
logger.debug(f"Vendor для MAC {normalized_mac}: {vendor}")
return vendor
except Exception as e:
logger.debug(f"Не удалось определить производителя для MAC {normalized_mac}: {str(e)}")
return "Unknown"
def get_hostname(self, ip: str) -> Optional[str]:
try:
return socket.gethostbyaddr(ip)[0]
except:
return None
class DeviceStorage:
"""Класс для хранения и управления картой устройств в JSON."""
def __init__(self, storage_path: str = "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]:
try:
if self.storage_file.exists():
with open(self.storage_file, "r", encoding="utf-8") as f:
data = json.load(f)
devices = {}
for device_data in data.get("devices", []):
device = Device(**device_data)
if device.mac:
devices[normalize_mac(device.mac)] = device
return self._clean_old_devices(devices)
return {}
except Exception as e:
logger.error(f"Ошибка при загрузке устройств: {str(e)}")
return {}
def _clean_old_devices(self, devices: Dict[str, Device]) -> Dict[str, Device]:
now = datetime.now(ZoneInfo("UTC"))
filtered = {}
for mac, device in devices.items():
if device.status == "offline" and device.last_seen:
try:
last_seen = datetime.fromisoformat(device.last_seen)
if now - last_seen <= self.offline_timeout:
filtered[mac] = device
else:
logger.info(f"Удалено устройство {mac} (offline > 7 дней)")
except ValueError:
logger.warning(f"Неверный формат last_seen для устройства {mac}")
else:
filtered[mac] = device
return filtered
def save_devices(self, devices: List[Device]) -> None:
try:
for device in devices:
if isinstance(device.last_seen, datetime):
device.last_seen = device.last_seen.isoformat()
if device.mac:
device.mac = normalize_mac(device.mac)
data = SubnetScanResult(devices=devices).model_dump()
with open(self.storage_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
logger.info(f"Сохранено {len(devices)} устройств в {self.storage_file}")
except Exception as e:
logger.error(f"Ошибка при сохранении устройств: {str(e)}")
class SubnetService:
"""Сервис для сканирования подсети и управления картой устройств."""
def __init__(self):
self.interface = self._get_default_interface()
self.network_scanner = NetworkScanner(self.interface)
self.device_info = DeviceInfoCollector()
self.port_scanner = PortScanner(self.interface, self.network_scanner.arp_cache)
self.storage = DeviceStorage()
def _get_default_interface(self):
"""Определяет основной сетевой интерфейс."""
try:
gateways = netifaces.gateways()
default_gateway = gateways.get("default", {}).get(netifaces.AF_INET)
if default_gateway:
return default_gateway[1]
for iface in netifaces.interfaces():
if iface != "lo":
return iface
return "eth0"
except Exception as e:
logger.warning(f"Не удалось определить интерфейс: {str(e)}")
return "eth0"
def scan_subnet(self, subnet: str, ports: List[int] = None) -> SubnetScanResult:
"""Сканирование подсети и результаты."""
start_time = time.time()
devices = []
now = datetime.now(ZoneInfo("UTC")).isoformat()
try:
saved_devices = self.storage.load_devices()
local_device = self._get_local_device(subnet)
if local_device:
local_device.status = "online"
local_device.last_seen = now
devices.append(local_device)
if local_device.mac:
saved_devices[normalize_mac(local_device.mac)] = local_device
discovered_devices = self.network_scanner.scan(subnet)
with ThreadPoolExecutor(max_workers=20) as executor: # Уменьшено до 20
def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]:
ip, mac = ip_mac
mac = normalize_mac(mac)
if local_device and ip == local_device.ip:
return None
existing_device = saved_devices.get(mac)
vendor = self.device_info.get_vendor(mac)
if vendor == "Unknown" and existing_device and existing_device.vendor != "Unknown":
vendor = existing_device.vendor
logger.debug(f"Использован сохранённый vendor для MAC {mac}: {vendor}")
return Device(
ip=ip,
mac=mac,
hostname=self.device_info.get_hostname(ip) or (
existing_device.hostname if existing_device else None),
vendor=vendor,
open_ports=self.port_scanner.scan(ip) or [],
status="online",
last_seen=now
)
device_results = executor.map(process_device, discovered_devices)
devices.extend([d for d in device_results if d is not None])
active_macs = {d.mac for d in devices if d.mac}
for mac, device in saved_devices.items():
if mac not in active_macs:
device.status = "offline"
devices.append(device)
devices.sort(key=lambda x: int(ipaddress.ip_address(x.ip)))
for device in devices:
if device.mac:
saved_devices[device.mac] = device
self.storage.save_devices(devices)
logger.info(
f"Сканирование подсети {subnet} завершено за {time.time() - start_time:.2f} сек, "
f"найдено {len(devices)} устройств (активных: {len(active_macs)})"
)
return SubnetScanResult(devices=devices)
except Exception as e:
logger.error(f"Ошибка при сканировании подсети {subnet}: {str(e)}")
return SubnetScanResult(devices=devices)
def _get_local_device(self, subnet: str) -> Optional[Device]:
"""Получает информацию о локальном устройстве."""
now = datetime.now(ZoneInfo("UTC")).isoformat()
try:
subnet_network = ipaddress.ip_network(subnet, strict=False)
for iface in netifaces.interfaces():
if iface == "lo":
continue
addrs = netifaces.ifaddresses(iface)
if netifaces.AF_INET in addrs and netifaces.AF_LINK in addrs:
ip = addrs[netifaces.AF_INET][0]["addr"]
if ipaddress.ip_address(ip) in subnet_network:
mac = normalize_mac(addrs[netifaces.AF_LINK][0]["addr"])
vendor = self.device_info.get_vendor(mac)
hostname = self.device_info.get_hostname(ip)
open_ports = self.port_scanner.scan(ip)
logger.debug(f"Локальное устройство: IP={ip}, MAC={mac}, Vendor={vendor}, Ports={open_ports}")
return Device(
ip=ip,
mac=mac,
hostname=hostname,
vendor=vendor,
open_ports=open_ports,
status="online",
last_seen=now
)
logger.warning(f"Локальное устройство не найдено в подсети {subnet}")
return None
except Exception as e:
logger.warning(f"Не удалось получить локальное устройство: {str(e)}")
return None

393
SRV/services/ufw_service.py Normal file
View file

@ -0,0 +1,393 @@
import ipaddress
import subprocess
from typing import List, Union
from models.ufw import Direction, Protocol, UFWRule, UFWRulesResponse, UFWStatus
DEBUG = True
class UFWService:
"""
Сервис для взаимодействия с утилитой UFW (Uncomplicated Firewall) в Linux.
Позволяет получить список правил UFW и управлять ими.
"""
def _run_command(self, command: str) -> str:
"""
Выполняет команду в системе и возвращает ее вывод.
Args:
command (str): Команда для выполнения в системе.
Returns:
str: Вывод команды.
"""
try:
if DEBUG:
print(f"Running command: {command}")
# Используем shell=True для корректной обработки аргументов с пробелами
result = subprocess.run(
f"sudo {command}",
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if DEBUG:
print(f"Command output: {result.stdout}")
print(f"Command error: {result.stderr}")
if result.returncode != 0:
raise subprocess.CalledProcessError(
result.returncode, command, result.stdout, result.stderr
)
return result.stdout
except subprocess.CalledProcessError as e:
if DEBUG:
print(f"Command failed with return code {e.returncode}")
print(f"Error output: {e.stderr}")
raise Exception(f"Command failed: {e.stderr}")
def _get_port_range_args(self, port: Union[int, str]) -> List[str]:
"""
Преобразует порт или диапазон портов в аргументы команды UFW.
Args:
port: Порт (число) или диапазон портов (строка вида "3000:3999")
Returns:
List[str]: Список аргументов для UFW
"""
if isinstance(port, int):
return [str(port)]
elif isinstance(port, str) and ":" in port:
# Диапазон портов уже в нужном формате
return [port]
else:
raise ValueError(f"Некорректный формат порта: {port}")
def get_ufw_rules(self) -> UFWRulesResponse:
"""
Получает список правил UFW.
Returns:
UFWRulesResponse: Объект, содержащий статус UFW и список правил
"""
# Check UFW status
status_result = self._run_command("ufw status numbered")
lines = status_result.splitlines()
if not lines:
return UFWRulesResponse(enabled=False, rules=[])
first_line = lines[0]
is_enabled = "Status: active" in first_line
if not is_enabled:
return UFWRulesResponse(enabled=False, rules=[])
rules = []
# Skip header lines
for line in lines[4:]: # Skip Status and header lines
if not line.strip() or not line.startswith("["):
continue
try:
rule = UFWRule.from_ufw_output(line)
rules.append(rule)
except Exception as e:
if DEBUG:
print(f"Ошибка парсинга правила: {line}, ошибка: {str(e)}")
continue
return UFWRulesResponse(enabled=is_enabled, rules=rules)
def get_ufw_status(self) -> UFWStatus:
"""
Проверяет статус UFW.
Returns:
UFWStatus: Объект с информацией о статусе UFW
"""
try:
status_result = self._run_command("ufw status")
first_line = (
status_result.splitlines()[0] if status_result.splitlines() else ""
)
is_enabled = "Status: active" in first_line
message = "UFW активен" if is_enabled else "UFW неактивен"
if DEBUG:
print(f"Результат статуса: {status_result}")
print(f"Включен: {is_enabled}")
return UFWStatus(enabled=is_enabled, message=message)
except Exception as e:
if DEBUG:
print(f"Ошибка при проверке статуса UFW: {e}")
return UFWStatus(
enabled=False,
message=f"Ошибка при проверке статуса: {str(e)}",
)
def enable_ufw(self) -> bool:
"""
Включает UFW.
Returns:
bool: True если успешно включен, False в противном случае
"""
try:
self._run_command("ufw enable")
return True
except Exception as e:
if DEBUG:
print(f"Ошибка при включении UFW: {e}")
return False
def disable_ufw(self) -> bool:
"""
Выключает UFW.
Returns:
bool: True если успешно выключен, False в противном случае
"""
try:
self._run_command("ufw disable")
return True
except Exception as e:
if DEBUG:
print(f"Ошибка при выключении UFW: {e}")
return False
def add_rules_base(self, rules: List[UFWRule], direction: str = "") -> bool:
"""
Базовый метод для добавления правил UFW.
Args:
rules: Список правил для добавления
direction: Направление трафика ("in" или "out")
Returns:
bool: True если все правила были успешно добавлены, False в противном случае
"""
try:
for rule in rules:
# Устанавливаем направление правила
rule.direction = Direction.IN if direction == "in" else Direction.OUT
# Формируем базовую команду
command = f"ufw {rule.action.value}"
# Добавляем направление
command += f" {rule.direction.value}"
# Добавляем from IP если он указан
if rule.from_ip and rule.from_ip != "any":
command += f" from {rule.from_ip}"
else:
command += " from any"
# Добавляем to IP если он указан
if rule.to_ip and rule.to_ip != "any":
command += f" to {rule.to_ip}"
else:
command += " to any"
# Добавляем порт если он указан
if rule.port:
command += f" port {rule.port}"
# Добавляем протокол
if rule.protocol != Protocol.BOTH:
command += f" proto {rule.protocol.value}"
# Добавляем комментарий если он есть
if rule.comment:
command += f" comment '{rule.comment}'"
self._run_command(command)
return True
except Exception as e:
if DEBUG:
print(f"Error adding UFW rules: {str(e)}")
return False
def add_ufw_in_rules(self, rules: List[UFWRule]) -> bool:
"""
Добавляет входящие (in) правила UFW.
Args:
rules (List[UFWRule]): Список правил для добавления
Returns:
bool: True если все правила были успешно добавлены, False в противном случае
"""
return self.add_rules_base(rules, "in")
def add_ufw_out_rules(self, rules: List[UFWRule]) -> bool:
"""
Добавляет исходящие (out) правила UFW.
Args:
rules (List[UFWRule]): Список правил для добавления
Returns:
bool: True если все правила были успешно добавлены, False в противном случае
"""
return self.add_rules_base(rules, "out")
def add_rules(self, rules: List[UFWRule]) -> dict:
"""
Добавляет правила UFW.
Args:
rules: Список правил для добавления
Returns:
dict: Результат добавления правил
"""
results = []
for rule in rules:
try:
# Определяем версию IP
from_ip = ipaddress.ip_network(rule.from_ip, strict=False)
is_ipv6 = isinstance(from_ip, ipaddress.IPv6Network)
# Получаем список протоколов
protocols = rule.protocol.protocols
print(f"Обработка правила для протоколов: {protocols}")
for proto in protocols:
# Формируем базовую команду
command = f"ufw {rule.action.value}"
# Добавляем IP адреса
command += f" from {rule.from_ip}"
if rule.to_ip:
command += f" to {rule.to_ip}"
# Добавляем порт и протокол
port_args = self._get_port_range_args(rule.port)
command += f" port {' '.join(port_args)} proto {proto}"
# Добавляем флаг IPv6 если нужно
if is_ipv6:
command += " v6"
# Добавляем комментарий если есть
if rule.comment:
command += f" comment '{rule.comment}'"
print(f"Подготовлена команда: {command}")
# Выполняем команду
try:
output = self._run_command(command)
results.append({"success": True, "message": output})
except Exception as e:
error_msg = f"Ошибка выполнения команды: {str(e)}"
print(error_msg)
results.append({"success": False, "message": error_msg})
except Exception as e:
error_msg = f"Ошибка при добавлении правила: {str(e)}"
print(error_msg)
results.append({"success": False, "message": error_msg})
# Выводим результаты
print("\nРезультаты добавления правил:")
for result in results:
status = "Успешно" if result["success"] else "Ошибка"
print(f" - {status}: {result['message']}")
return {"success": True, "results": results}
def delete_rules(self, rule_numbers: List[int]) -> dict:
"""
Удаляет правила UFW по их номерам.
Args:
rule_numbers (List[int]): Список номеров правил для удаления
Returns:
dict: Результат операции удаления
"""
try:
# Проверяем существование правил
status_result = self._run_command("ufw status numbered")
lines = status_result.splitlines()
# Пропускаем заголовок (первые 4 строки)
rules = []
for line in lines[4:]: # Skip header lines
if not line.strip() or not line.startswith("["):
continue
try:
# Извлекаем номер правила из строки [N]
rule_num = int(line[1 : line.index("]")])
rules.append(rule_num)
except (ValueError, IndexError):
continue
if DEBUG:
print(f"Найдены правила: {rules}")
results = []
# Сортируем номера в обратном порядке, чтобы удаление происходило с конца
# Это предотвратит смещение номеров при удалении
for rule_number in sorted(rule_numbers, reverse=True):
try:
# Проверяем существует ли правило с таким номером
if rule_number not in rules:
results.append(
{
"number": rule_number,
"success": False,
"error": f"Правило с номером {rule_number} не найдено",
}
)
continue
# Удаляем правило
self._run_command(f"ufw --force delete {rule_number}")
results.append(
{
"number": rule_number,
"success": True,
"message": f"Правило {rule_number} успешно удалено",
}
)
except Exception as e:
if DEBUG:
print(f"Ошибка при удалении правила {rule_number}: {e}")
results.append(
{"number": rule_number, "success": False, "error": str(e)}
)
# Подсчитываем статистику
total = len(rule_numbers)
successful = len([r for r in results if r["success"]])
return {
"success": True,
"total": total,
"successful": successful,
"results": results,
}
except Exception as e:
if DEBUG:
print(f"Ошибка при удалении правил: {e}")
return {"success": False, "error": str(e), "results": []}

View file

@ -0,0 +1,227 @@
import time
import logging
import subprocess
import re
from functools import lru_cache
from typing import List, Optional, Dict
from mac_vendor_lookup import MacLookup
import pywifi
from models.wifi import WifiAccessPoint, WifiScanResult
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
class WifiService:
"""Сервис для сканирования текущих Wi-Fi точек доступа"""
def __init__(self):
self.mac_lookup = MacLookup()
self.wifi = pywifi.PyWiFi()
self.interface = self._get_wifi_interface()
def _get_wifi_interface(self) -> Optional[object]:
"""Автоматически определяет активный Wi-Fi-интерфейс"""
try:
interfaces = self.wifi.interfaces()
if not interfaces:
logger.error("Не найдено сетевых интерфейсов")
return None
logger.debug(f"Доступные интерфейсы: {[iface.name() for iface in interfaces]}")
for iface in interfaces:
iface_name = iface.name()
if 'p2p' in iface_name.lower(): # без p2p, потом в любом случае надо будет исправлять думаю это не точное решение
logger.debug(f"Пропущен интерфейс {iface_name} (P2P)")
continue
try:
iface.scan()
time.sleep(1)
results = iface.scan_results()
if results:
logger.info(f"Выбран Wi-Fi-интерфейс: {iface_name}")
return iface
logger.debug(f"Интерфейс {iface_name} не вернул результатов сканирования")
except Exception as e:
logger.debug(f"Интерфейс {iface_name} неактивен: {str(e)}")
logger.error("Не найден активный Wi-Fi-интерфейс")
return None
except Exception as e:
logger.error(f"Ошибка при определении Wi-Fi-интерфейса: {str(e)}")
return None
@lru_cache(maxsize=1000)
def get_vendor(self, mac: str) -> str:
try:
return self.mac_lookup.lookup(mac)
except Exception as e:
logger.warning(f"Не удалось определить производителя для MAC {mac}: {str(e)}")
return "Unknown"
def _get_encryption_from_iw(self, ap_info: Dict) -> str:
"""Определяет шифрование из данных iw"""
security = ap_info.get("security", "")
logger.debug(f"Шифрование для BSSID {ap_info.get('bssid')}: {security}")
if "WPA3" in security:
return "WPA3"
elif "WPA2" in security or "RSN" in security:
return "WPA2"
elif "WPA" in security:
return "WPA"
elif "WEP" in security:
return "WEP"
return "Open"
def _get_channel(self, frequency: Optional[int]) -> Optional[int]:
"""Преобразует частоту в канал."""
if frequency is None:
return None
try:
if 2412 <= frequency <= 2472:
return (frequency - 2412) // 5 + 1
elif 5180 <= frequency <= 5825:
return (frequency - 5180) // 5 + 36
logger.debug(f"Частота {frequency} вне диапазона 2.4/5 GHz")
return None
except Exception as e:
logger.debug(f"Ошибка при определении канала для частоты {frequency}: {str(e)}")
return None
def _get_frequency(self, frequency: Optional[int]) -> Optional[str]:
"""Определяет частотный диапазон."""
if frequency is None:
return None
try:
if 2412 <= frequency <= 2472:
return "2.4 GHz"
elif 5180 <= frequency <= 5825:
return "5 GHz"
logger.debug(f"Частота {frequency} вне диапазона 2.4/5 GHz")
return None
except Exception as e:
logger.debug(f"Ошибка при определении диапазона для частоты {frequency}: {str(e)}")
return None
def _get_iw_scan_data(self) -> List[Dict]:
"""Получает данные сканирования через iw."""
try:
if not self.interface:
logger.error("Интерфейс не определён")
return []
cmd = ["sudo", "iw", "dev", self.interface.name(), "scan"]
logger.debug(f"Выполняем команду: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode != 0:
logger.error(f"Ошибка iw scan: {result.stderr}")
return []
output = result.stdout
logger.debug(f"Вывод iw scan (первые 500 символов): {output[:500]}...")
aps = []
current_ap = None
bssid_pattern = re.compile(r"BSS ([0-9a-f:]{17})")
ssid_pattern = re.compile(r"SSID: ?(.*)")
freq_pattern = re.compile(r"freq: (\d+)")
signal_pattern = re.compile(r"signal: ([-\d.]+) dBm")
rsn_pattern = re.compile(r"RSN:")
wpa_pattern = re.compile(r"WPA:")
security_type_pattern = re.compile(r"\* (WPA3|WPA2|WPA|WEP)")
for line in output.splitlines():
line = line.strip()
if bssid_match := bssid_pattern.match(line):
if current_ap:
aps.append(current_ap)
current_ap = {"bssid": bssid_match.group(1).lower(), "security": ""}
elif current_ap:
if ssid_match := ssid_pattern.match(line):
current_ap["ssid"] = ssid_match.group(1) if ssid_match.group(1).strip() else ""
elif freq_match := freq_pattern.match(line):
current_ap["frequency"] = int(freq_match.group(1))
elif signal_match := signal_pattern.match(line):
current_ap["signal"] = float(signal_match.group(1))
elif rsn_pattern.match(line):
current_ap["security"] += "RSN;"
elif wpa_pattern.match(line):
current_ap["security"] += "WPA;"
elif security_type_match := security_type_pattern.search(line):
current_ap["security"] += security_type_match.group(1) + ";"
if current_ap:
aps.append(current_ap)
logger.debug(f"Найдено {len(aps)} точек через iw: {[ap['bssid'] for ap in aps]}")
for ap in aps:
logger.debug(f"Данные точки {ap['bssid']}: {ap}")
return aps
except subprocess.CalledProcessError as e:
logger.error(f"Ошибка при выполнении iw scan: {e.stderr}")
return []
except subprocess.TimeoutExpired:
logger.error("Таймаут при выполнении iw scan")
return []
except Exception as e:
logger.error(f"Ошибка при парсинге iw scan: {str(e)}")
return []
def scan_wifi(self, timeout: int = 30) -> WifiScanResult:
"""Сканирует текущие Wi-Fi точки доступа"""
start_time = time.time()
aps = []
if not self.interface:
logger.error("Wi-Fi-интерфейс не доступен")
return WifiScanResult(access_points=aps)
try:
logger.info(f"Начало сканирования Wi-Fi на интерфейсе {self.interface.name()}")
self.interface.scan()
time.sleep(10) # Нативное значение для скана сети (в секундах). Оптимальное для точного сканирования хватает от 5 до 20 секунд
py_results = self.interface.scan_results()
logger.debug(f"Найдено {len(py_results)} точек через pywifi")
iw_results = self._get_iw_scan_data()
iw_by_bssid = {ap['bssid']: ap for ap in iw_results if ap.get('bssid')}
for ap in py_results:
bssid = ap.bssid.lower() if ap.bssid else None
if not bssid:
logger.debug("Пропущена точка без BSSID")
continue
ssid = ap.ssid if ap.ssid else ""
hidden = not ssid
signal = ap.signal if ap.signal else None
iw_data = iw_by_bssid.get(bssid, {})
frequency = iw_data.get("frequency")
channel = self._get_channel(frequency)
freq_band = self._get_frequency(frequency)
encryption = self._get_encryption_from_iw(iw_data)
vendor = self.get_vendor(bssid)
logger.debug(
f"Точка {bssid}: freq={frequency}, channel={channel}, encryption={encryption}, signal={signal}")
ap_data = WifiAccessPoint(
ssid=ssid,
bssid=bssid,
channel=channel,
signal=signal if signal else iw_data.get("signal"),
encryption=encryption,
frequency=freq_band,
vendor=vendor,
hidden=hidden
)
aps.append(ap_data)
logger.info(
f"Сканирование Wi-Fi завершено за {time.time() - start_time:.2f} секунд, "
f"найдено {len(aps)} точек"
)
return WifiScanResult(access_points=aps)
except Exception as e:
logger.error(f"Ошибка при сканировании Wi-Fi: {str(e)}")
return WifiScanResult(access_points=aps)