Версия 0.0.3
This commit is contained in:
parent
413e9c57be
commit
8c7d0609df
189 changed files with 52063 additions and 2769 deletions
|
|
@ -3,20 +3,27 @@ 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']
|
||||
"""Инициализация параметров интерфейсов"""
|
||||
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
|
||||
return ether / arp
|
||||
|
||||
def scan_network(self, attempts=3, timeout=2):
|
||||
"""Подготовка к сканированию сети"""
|
||||
|
|
@ -28,9 +35,12 @@ class NetworkScanner:
|
|||
clients = []
|
||||
|
||||
for _ in range(attempts):
|
||||
result = srp(packet, timeout=timeout, iface=self.interface, verbose=0)[0]
|
||||
result = srp(packet,
|
||||
timeout=timeout,
|
||||
iface=self.interface,
|
||||
verbose=0)[0]
|
||||
for _, received in result:
|
||||
client = {'ip': received.psrc, 'mac': received.hwsrc}
|
||||
client = {"ip": received.psrc, "mac": received.hwsrc}
|
||||
if client not in clients: # Избегаем дублирования клиентов
|
||||
clients.append(client)
|
||||
|
||||
|
|
@ -38,7 +48,7 @@ class NetworkScanner:
|
|||
|
||||
# Добавляет все локальные IP-адреса интерфейса как клиенты
|
||||
for ip_info in self.ip_info:
|
||||
local_client = {'ip': ip_info['addr'], 'mac': self.local_mac}
|
||||
local_client = {"ip": ip_info["addr"], "mac": self.local_mac}
|
||||
if local_client not in all_clients:
|
||||
all_clients.append(local_client)
|
||||
|
||||
|
|
@ -49,10 +59,11 @@ class NetworkScanner:
|
|||
def print_clients(self, clients):
|
||||
"""Лист адресов клиентов"""
|
||||
print("Доступные устройства в сети:")
|
||||
print("IP" + " "*18+"MAC")
|
||||
for client in sorted(clients, key=lambda x: x['ip']):
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
|||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""Сервис для регистрации, авторизации и управления сессиями"""
|
||||
|
||||
def __init__(self):
|
||||
self.storage = JsonStorage()
|
||||
self.session_storage = SessionStorage()
|
||||
|
|
@ -35,7 +37,8 @@ class AuthService:
|
|||
def create_access_token(self, data: dict) -> str:
|
||||
"""Создаёт токен"""
|
||||
to_encode = data.copy()
|
||||
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
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
|
||||
|
|
@ -57,10 +60,14 @@ class AuthService:
|
|||
db_user = self.storage.create_user(user)
|
||||
return db_user
|
||||
except ValueError as e:
|
||||
logger.error(f"Ошибка при регистрации пользователя {user.username}: {str(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)}")
|
||||
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:
|
||||
|
|
@ -69,24 +76,41 @@ class AuthService:
|
|||
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)
|
||||
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"])
|
||||
"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)}")
|
||||
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:
|
||||
def get_current_user(
|
||||
self, token: str = Depends(oauth2_scheme)) -> UserResponse:
|
||||
"""Получает текущего пользователя по токену"""
|
||||
credentials = self.verify_token(token)
|
||||
if not credentials:
|
||||
|
|
@ -96,24 +120,36 @@ class AuthService:
|
|||
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"])
|
||||
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="Сессия недействительна")
|
||||
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="Пользователь не найден")
|
||||
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
|
||||
"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):
|
||||
|
|
@ -122,8 +158,12 @@ class AuthService:
|
|||
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}")
|
||||
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="Ошибка при выходе из системы")
|
||||
logger.error(
|
||||
f"Ошибка при выходе из системы: {type(e).__name__} - {str(e)}")
|
||||
raise HTTPException(status_code=500,
|
||||
detail="Ошибка при выходе из системы")
|
||||
|
|
|
|||
106
SRV/services/connect_service.py
Normal file
106
SRV/services/connect_service.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import subprocess
|
||||
import logging
|
||||
from typing import Tuple, Optional
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WifiConnectionService:
|
||||
def __init__(self):
|
||||
self.interface = self._get_wifi_interface()
|
||||
|
||||
def _get_wifi_interface(self) -> str:
|
||||
"""Автоматически определяет Wi-Fi-интерфейс."""
|
||||
try:
|
||||
result = subprocess.run(["nmcli", "radio", "wifi"], capture_output=True, text=True)
|
||||
if "enabled" in result.stdout.lower():
|
||||
interfaces = subprocess.run(["nmcli", "device", "status"], capture_output=True, text=True).stdout
|
||||
for line in interfaces.splitlines():
|
||||
if "wifi" in line.lower() and "connected" in line.lower():
|
||||
iface = line.split()[0]
|
||||
logger.info(f"Используется Wi-Fi-интерфейс: {iface}")
|
||||
return iface
|
||||
logger.warning("Wi-Fi-интерфейс не найден или не подключён")
|
||||
return "wlan0"
|
||||
logger.error("Wi-Fi радио отключено")
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при определении Wi-Fi-интерфейса: {str(e)}")
|
||||
return ""
|
||||
|
||||
def connect(self, ssid: str, password: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Подключается к Wi-Fi сети по SSID и паролю."""
|
||||
try:
|
||||
if not self.interface:
|
||||
return False, "Wi-Fi-интерфейс не доступен"
|
||||
|
||||
result = subprocess.run(["nmcli", "-f", "SSID", "device", "wifi", "list"], capture_output=True, text=True)
|
||||
if ssid not in result.stdout:
|
||||
return False, f"Сеть с SSID {ssid} не найдена в списке доступных сетей"
|
||||
|
||||
cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]
|
||||
process = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||
if process.returncode != 0:
|
||||
error_msg = process.stderr.lower()
|
||||
if "secrets were required" in error_msg or "authentication failed" in error_msg or "802-11-wireless-security.psk: недействительное свойство" in process.stderr:
|
||||
return False, f"Ошибка подключения к {ssid}: Неверный пароль"
|
||||
elif "no network with ssid" in error_msg:
|
||||
return False, f"Ошибка подключения к {ssid}: Сеть с SSID {ssid} недоступна или неверно указана"
|
||||
else:
|
||||
return False, f"Ошибка подключения к {ssid}: {process.stderr}"
|
||||
logger.info(f"Успешное подключение к сети {ssid}")
|
||||
return True, None
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка подключения к {ssid}: {e.stderr}")
|
||||
error_msg = e.stderr.lower()
|
||||
if "secrets were required" in error_msg or "authentication failed" in error_msg or "802-11-wireless-security.psk: недействительное свойство" in e.stderr:
|
||||
return False, f"Ошибка подключения к {ssid}: Неверный пароль"
|
||||
elif "no network with ssid" in error_msg:
|
||||
return False, f"Ошибка подключения к {ssid}: Сеть с SSID {ssid} недоступна или неверно указана"
|
||||
return False, f"Ошибка подключения к {ssid}: {e.stderr}"
|
||||
except Exception as e:
|
||||
logger.error(f"Неизвестная ошибка при подключении к {ssid}: {str(e)}")
|
||||
return False, f"Ошибка подключения к {ssid}: Неизвестная ошибка - {str(e)}"
|
||||
|
||||
def disconnect(self, ssid: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Отключается от Wi-Fi сети по указанному SSID."""
|
||||
try:
|
||||
if not self.interface:
|
||||
return False, "Wi-Fi-интерфейс не доступен"
|
||||
result = subprocess.run(["nmcli", "connection", "show", "--active"], capture_output=True, text=True)
|
||||
active_connections = result.stdout.strip().splitlines()
|
||||
if not active_connections:
|
||||
logger.info(f"Нет активных подключений для SSID {ssid}")
|
||||
return True, f"Нет активного подключения для SSID {ssid}"
|
||||
for conn in active_connections:
|
||||
conn_details = conn.split()
|
||||
if len(conn_details) > 1:
|
||||
conn_name = conn_details[0]
|
||||
conn_info = subprocess.run(["nmcli", "connection", "show", conn_name], capture_output=True, text=True).stdout
|
||||
if "802-11-wireless.ssid:" in conn_info and ssid in conn_info:
|
||||
subprocess.run(["nmcli", "connection", "down", conn_name], capture_output=True, text=True, check=True)
|
||||
logger.info(f"Отключено соединение с SSID {ssid}")
|
||||
return True, None
|
||||
logger.warning(f"Активное подключение с SSID {ssid} не найдено")
|
||||
return True, f"Активное подключение с SSID {ssid} не найдено"
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка при отключении от {ssid}: {e.stderr}")
|
||||
return False, f"Ошибка отключения от {ssid}: {e.stderr}"
|
||||
except Exception as e:
|
||||
logger.error(f"Неизвестная ошибка при отключении от {ssid}: {str(e)}")
|
||||
return False, f"Ошибка отключения от {ssid}: {str(e)}"
|
||||
|
||||
def get_status(self) -> Tuple[bool, Optional[str]]:
|
||||
"""Проверяет статус текущего подключения к Wi-Fi."""
|
||||
try:
|
||||
if not self.interface:
|
||||
return False, "Wi-Fi-интерфейс не доступен"
|
||||
result = subprocess.run(["nmcli", "connection", "show", "--active"], capture_output=True, text=True)
|
||||
if result.stdout.strip():
|
||||
ssid = result.stdout.splitlines()[1].split()[0]
|
||||
logger.info(f"Текущая сеть: {ssid}")
|
||||
return True, ssid
|
||||
return False, "Не подключено к Wi-Fi"
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке статуса: {str(e)}")
|
||||
return False, f"Ошибка проверки статуса: {str(e)}"
|
||||
|
|
@ -7,11 +7,11 @@ DEBUG = True
|
|||
|
||||
|
||||
class FirewalldService:
|
||||
|
||||
def __init__(self):
|
||||
self.bus = SystemBus()
|
||||
self.proxy = self.bus.get(
|
||||
"org.fedoraproject.FirewallD1", "/org/fedoraproject/FirewallD1"
|
||||
)
|
||||
self.proxy = self.bus.get("org.fedoraproject.FirewallD1",
|
||||
"/org/fedoraproject/FirewallD1")
|
||||
|
||||
def get_zones(self) -> List[str]:
|
||||
"""Get list of all zones."""
|
||||
|
|
@ -34,12 +34,16 @@ class FirewalldService:
|
|||
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:
|
||||
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)}")
|
||||
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."""
|
||||
|
|
@ -55,22 +59,23 @@ class FirewalldService:
|
|||
try:
|
||||
return self.proxy.getServices(zone)
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to get services for zone {zone}: {str(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:
|
||||
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']
|
||||
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
|
||||
|
|
@ -106,7 +111,8 @@ class FirewalldService:
|
|||
try:
|
||||
self.proxy.addSource(zone, source)
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to add source {source} to zone {zone}: {str(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."""
|
||||
|
|
@ -114,8 +120,7 @@ class FirewalldService:
|
|||
self.proxy.removeSource(zone, source)
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
f"Failed to remove source {source} from zone {zone}: {str(e)}"
|
||||
)
|
||||
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."""
|
||||
|
|
@ -140,4 +145,5 @@ class FirewalldService:
|
|||
"sources": self.get_sources(zone),
|
||||
}
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to get zone settings for {zone}: {str(e)}")
|
||||
raise Exception(
|
||||
f"Failed to get zone settings for {zone}: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -1,53 +1,66 @@
|
|||
from sdbus_async.networkmanager import NetworkManager, NetworkManagerSettings, NetworkConnectionSettings, NetworkDeviceGeneric, ActiveConnection
|
||||
import asyncio
|
||||
from sdbus_async.networkmanager.settings import ConnectionProfile
|
||||
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 """
|
||||
"""Перезапускает соединение с системной шиной 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:
|
||||
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-адреса перед его применением """
|
||||
"""Проверяет доступность 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."}
|
||||
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 """
|
||||
"""Применяет проверенные настройки IP"""
|
||||
self._reset_bus()
|
||||
nm = NetworkManager(self.bus)
|
||||
settings = NetworkManagerSettings(self.bus)
|
||||
connection_paths = await settings.get_connections_by_id(args["connection_id"])
|
||||
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], self.bus)
|
||||
connection = NetworkConnectionSettings(connection_paths[0])
|
||||
profile = await connection.get_profile()
|
||||
settings_dict = profile.to_settings_dict()
|
||||
|
||||
# Обновляем настройки IPv4
|
||||
for key_path, value in args["ipv4_updates"].items():
|
||||
section, *props = key_path.split('.')
|
||||
section, *props = key_path.split(".")
|
||||
current = settings_dict[section]
|
||||
for prop in props[:-1]:
|
||||
current = current.setdefault(prop, {})
|
||||
|
|
@ -57,56 +70,17 @@ class IPService:
|
|||
await connection.update(new_profile.to_dbus())
|
||||
print(f"Обновлены настройки для {args['connection_id']}:")
|
||||
|
||||
# Проверяем interface-name
|
||||
interface_name = settings_dict['connection'].get('interface-name')
|
||||
if not interface_name:
|
||||
# Ищем устройство, связанное с connection_id через активное соединение
|
||||
devices = await nm.get_devices()
|
||||
for device_path in devices:
|
||||
device = NetworkDeviceGeneric(device_path, self.bus)
|
||||
active_connection = await device.active_connection
|
||||
if active_connection != '/':
|
||||
active_conn = ActiveConnection(active_connection, self.bus)
|
||||
conn_path = await active_conn.connection # Путь к настройкам соединения
|
||||
if conn_path != '/':
|
||||
conn_settings = NetworkConnectionSettings(conn_path, self.bus)
|
||||
active_profile = await conn_settings.get_profile()
|
||||
if active_profile.to_settings_dict()['connection']['id'] == args["connection_id"]:
|
||||
interface_name = await device.interface
|
||||
settings_dict['connection']['interface-name'] = interface_name
|
||||
new_profile = ConnectionProfile.from_settings_dict(settings_dict)
|
||||
await connection.update(new_profile.to_dbus())
|
||||
print(f"Добавлен interface-name: {interface_name} для {args['connection_id']}")
|
||||
break
|
||||
|
||||
if not interface_name:
|
||||
# Если не нашли активное соединение, пробуем получить интерфейс из настроек устройства
|
||||
devices = await nm.get_devices()
|
||||
for device_path in devices:
|
||||
device = NetworkDeviceGeneric(device_path, self.bus)
|
||||
applied_connection = await device.get_applied_connection()
|
||||
connection_settings = applied_connection[0]
|
||||
conn_id = connection_settings.get('connection', {}).get('id', ('s', ''))[1]
|
||||
if conn_id == args["connection_id"]:
|
||||
interface_name = await device.interface
|
||||
settings_dict['connection']['interface-name'] = interface_name
|
||||
new_profile = ConnectionProfile.from_settings_dict(settings_dict)
|
||||
await connection.update(new_profile.to_dbus())
|
||||
print(f"Добавлен interface-name: {interface_name} для {args['connection_id']}")
|
||||
break
|
||||
|
||||
if not interface_name:
|
||||
raise ValueError(f"Не удалось определить interface-name для {args['connection_id']}")
|
||||
|
||||
# Переподключаем устройство
|
||||
interface_name = settings_dict["connection"]["interface-name"]
|
||||
devices = await nm.get_devices()
|
||||
for device_path in devices:
|
||||
device = NetworkDeviceGeneric(device_path, self.bus)
|
||||
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)
|
||||
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} не найдено")
|
||||
|
|
|
|||
|
|
@ -1,26 +1,27 @@
|
|||
from typing import List, Dict, Tuple, Union, Optional
|
||||
from sdbus_async.networkmanager import (
|
||||
NetworkManager,
|
||||
NetworkDeviceGeneric,
|
||||
IPv4Config,
|
||||
IPv6Config,
|
||||
NetworkManagerSettings,
|
||||
NetworkDeviceWireless,
|
||||
AccessPoint,
|
||||
DeviceType
|
||||
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
|
||||
NetworkInterface,
|
||||
IPv4Route,
|
||||
IPv6Route,
|
||||
IPConfig,
|
||||
IndividualIPConfig,
|
||||
NetworkInterfacesContainer,
|
||||
WiFiDevice,
|
||||
)
|
||||
import subprocess
|
||||
|
||||
|
||||
class NetworkService:
|
||||
"""Сервис для взаимодействия с NetworkManager через D-Bus"""
|
||||
|
||||
|
|
@ -29,76 +30,99 @@ class NetworkService:
|
|||
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:
|
||||
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])
|
||||
return str(settings.get("method", ("s", "unknown"))[1])
|
||||
except Exception:
|
||||
return 'unknown'
|
||||
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])
|
||||
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'
|
||||
return "unknown"
|
||||
|
||||
async def get_ip_config(self, device: NetworkDeviceGeneric, ip_version: str) -> List[IndividualIPConfig]:
|
||||
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)
|
||||
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]),
|
||||
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]
|
||||
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}'),
|
||||
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')]
|
||||
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]]]:
|
||||
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
|
||||
"ssid": None,
|
||||
"bssid": None,
|
||||
"bitrate": await wifi_device.bitrate,
|
||||
"strength": None,
|
||||
"frequency": None,
|
||||
"mode": None,
|
||||
}
|
||||
if active_ap != '/':
|
||||
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
|
||||
"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]]:
|
||||
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 = []
|
||||
|
|
@ -109,30 +133,44 @@ class NetworkService:
|
|||
interface_name = str(await generic_device.interface)
|
||||
device_type = DeviceType(await generic_device.device_type).name
|
||||
|
||||
if device_type == 'LOOPBACK':
|
||||
if device_type == "LOOPBACK":
|
||||
continue
|
||||
|
||||
device_state = str(await generic_device.state)
|
||||
if device_state == '10':
|
||||
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],
|
||||
"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))
|
||||
|
||||
|
|
@ -141,7 +179,10 @@ class NetworkService:
|
|||
async def get_hostname(self) -> Dict[str, str]:
|
||||
"""Возвращает имя хоста из команды hostname в формате JSON"""
|
||||
try:
|
||||
result = subprocess.run(['hostname'], capture_output=True, text=True, check=True)
|
||||
result = subprocess.run(["hostname"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True)
|
||||
hostname = result.stdout.strip()
|
||||
return {"hostname": hostname}
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
|
@ -149,52 +190,83 @@ class NetworkService:
|
|||
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]:
|
||||
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]
|
||||
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:
|
||||
""" Получает объект, содержащий список интерфейсов и координаты контейнера """
|
||||
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)
|
||||
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
|
||||
)
|
||||
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:
|
||||
""" Получает контейнер с сетевыми интерфейсами и координатами контейнера """
|
||||
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
|
||||
)
|
||||
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_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]:
|
||||
""" Получает список неактивных сетевых интерфейсов """
|
||||
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']
|
||||
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_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:
|
||||
""" Получает объект, содержащий список неактивных интерфейсов и координаты контейнера """
|
||||
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
|
||||
)
|
||||
return NetworkInterfacesContainer(interfaces=interfaces,
|
||||
container_x=container_x,
|
||||
container_y=container_y)
|
||||
|
|
|
|||
424
SRV/services/subnet_service.py
Normal file
424
SRV/services/subnet_service.py
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
from scapy.config import conf
|
||||
from mac_vendor_lookup import MacLookup
|
||||
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 subprocess
|
||||
|
||||
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__)
|
||||
|
||||
class NetworkScanner:
|
||||
def __init__(self, interface: str):
|
||||
self.interface = interface
|
||||
conf.iface = interface
|
||||
|
||||
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=2,
|
||||
verbose=False,
|
||||
iface=self.interface)[0]
|
||||
return [(received.psrc, received.hwsrc)
|
||||
for sent, received in result]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка ARP-сканирования подсети {subnet}: {str(e)}")
|
||||
return []
|
||||
|
||||
class PortScanner:
|
||||
def __init__(self, interface: str):
|
||||
self.interface = interface
|
||||
|
||||
def scan(self, ip: str) -> List[str]:
|
||||
open_ports = []
|
||||
|
||||
def check_port(port: int) -> Optional[str]:
|
||||
try:
|
||||
pkt = sr1(
|
||||
IP(dst=ip) / TCP(dport=port, flags="S"),
|
||||
timeout=2,
|
||||
verbose=False,
|
||||
)
|
||||
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()), 15)
|
||||
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 = MacLookup()
|
||||
self.nbtscan_cache = {}
|
||||
self.nbtscan_results = {}
|
||||
|
||||
@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 scan_nbtscan(self, subnet: str) -> None:
|
||||
if subnet not in self.nbtscan_results:
|
||||
try:
|
||||
logger.debug(f"Запуск nbtscan для подсети {subnet}")
|
||||
result = subprocess.run(["sudo", "nbtscan", "-r", str(subnet)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=10)
|
||||
logger.debug(f"Полный вывод nbtscan: {result.stdout}")
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.splitlines():
|
||||
logger.debug(f"Обработка строки: {line}")
|
||||
if not line.startswith(("IP address", "-", "Doing NBT")):
|
||||
parts = [p for p in line.split() if p]
|
||||
if len(parts) >= 2:
|
||||
ip_addr, hostname = parts[0], parts[1]
|
||||
if hostname != "<unknown>":
|
||||
self.nbtscan_cache[ip_addr] = hostname
|
||||
logger.info(f"NetBIOS: Найден хостнейм {hostname} для IP {ip_addr}")
|
||||
self.nbtscan_results[subnet] = True
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error(f"NetBIOS: Таймаут для подсети {subnet}")
|
||||
except Exception as e:
|
||||
logger.error(f"NetBIOS: Ошибка для подсети {subnet}: {str(e)}")
|
||||
|
||||
def get_hostname(self, ip: str) -> Optional[str]:
|
||||
try:
|
||||
socket.setdefaulttimeout(2)
|
||||
return socket.gethostbyaddr(ip)[0]
|
||||
except (socket.herror, socket.gaierror, socket.timeout):
|
||||
logger.debug(f"DNS: Не удалось разрешить имя для IP {ip}")
|
||||
|
||||
subnet = ipaddress.ip_network(f"{ip}/24", strict=False)
|
||||
if subnet not in self.nbtscan_results:
|
||||
self.scan_nbtscan(str(subnet))
|
||||
|
||||
if ip in self.nbtscan_cache:
|
||||
return self.nbtscan_cache[ip]
|
||||
|
||||
logger.debug(f"Не удалось определить хостнейм для IP {ip} через DNS или NetBIOS")
|
||||
return None
|
||||
|
||||
class DeviceStorage:
|
||||
def __init__(self, storage_path: str = "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]:
|
||||
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[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()
|
||||
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:
|
||||
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.port_scanner = PortScanner(self.interface)
|
||||
self.device_info = DeviceInfoCollector()
|
||||
self.storage = DeviceStorage()
|
||||
|
||||
def _get_default_interface(self) -> str:
|
||||
try:
|
||||
gateways = netifaces.gateways()
|
||||
default_gateway = gateways.get("default", {}).get(netifaces.AF_INET)
|
||||
if default_gateway:
|
||||
return default_gateway[1]
|
||||
interfaces = netifaces.interfaces()
|
||||
for iface in 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().astimezone()
|
||||
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
|
||||
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 = formatted_now
|
||||
devices.append(local_device)
|
||||
if local_device.mac:
|
||||
saved_devices[local_device.mac] = local_device
|
||||
|
||||
discovered_devices = self.network_scanner.scan(subnet)
|
||||
self.device_info.scan_nbtscan(subnet)
|
||||
|
||||
def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]:
|
||||
ip, mac = ip_mac
|
||||
if local_device and ip == local_device.ip:
|
||||
return None
|
||||
|
||||
existing_device = saved_devices.get(mac)
|
||||
if existing_device:
|
||||
return Device(
|
||||
ip=ip,
|
||||
mac=mac,
|
||||
hostname=self.device_info.get_hostname(ip) or existing_device.hostname,
|
||||
vendor=self.device_info.get_vendor(mac),
|
||||
open_ports=self.port_scanner.scan(ip) or existing_device.open_ports,
|
||||
status="online",
|
||||
last_seen=formatted_now,
|
||||
note=existing_device.note,
|
||||
)
|
||||
vendor = self.device_info.get_vendor(mac)
|
||||
hostname = self.device_info.get_hostname(ip)
|
||||
open_ports = self.port_scanner.scan(ip)
|
||||
return Device(
|
||||
ip=ip,
|
||||
mac=mac,
|
||||
hostname=hostname,
|
||||
vendor=vendor,
|
||||
open_ports=open_ports,
|
||||
status="online",
|
||||
last_seen=formatted_now,
|
||||
note=None,
|
||||
)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
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.IPv4Address(x.ip)))
|
||||
|
||||
for device in devices:
|
||||
if device.mac:
|
||||
saved_devices[device.mac] = device
|
||||
self.storage.save_devices(list(saved_devices.values()))
|
||||
|
||||
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().astimezone()
|
||||
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
|
||||
try:
|
||||
subnet_network = ipaddress.ip_network(subnet, strict=False)
|
||||
interfaces = netifaces.interfaces()
|
||||
for iface in 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 = 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}, Ports={open_ports}, Hostname={hostname}"
|
||||
)
|
||||
return Device(
|
||||
ip=ip,
|
||||
mac=mac,
|
||||
hostname=hostname,
|
||||
vendor=vendor,
|
||||
open_ports=open_ports,
|
||||
status="online",
|
||||
last_seen=formatted_now,
|
||||
note=None,
|
||||
)
|
||||
logger.warning(
|
||||
f"Локальное устройство не найдено в подсети {subnet}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Не удалось получить локальное устройство: {str(e)}")
|
||||
return None
|
||||
|
||||
def add_note(self, mac: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Добавляет заметку к устройству по MAC-адресу.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
note (str): Текст заметки.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении.
|
||||
"""
|
||||
try:
|
||||
saved_devices = self.storage.load_devices()
|
||||
if mac not in saved_devices:
|
||||
logger.warning(f"Устройство с MAC {mac} не найдено")
|
||||
return False, f"Устройство с MAC {mac} не найдено"
|
||||
if saved_devices[mac].note is not None:
|
||||
logger.error(
|
||||
f"Заметка для устройства с MAC {mac} уже существует")
|
||||
return False, f"Заметка для устройства с MAC {mac} уже существует"
|
||||
saved_devices[mac].note = note
|
||||
self.storage.save_devices(list(saved_devices.values()))
|
||||
logger.info(
|
||||
f"Добавлена заметка '{note}' для устройства с MAC {mac}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка при добавлении заметки для MAC {mac}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
|
||||
def update_note(self, mac: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Обновляет заметку устройства по MAC-адресу.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
note (str): Новый текст заметки.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении.
|
||||
"""
|
||||
try:
|
||||
saved_devices = self.storage.load_devices()
|
||||
if mac not in saved_devices:
|
||||
logger.warning(f"Устройство с MAC {mac} не найдено")
|
||||
return False, f"Устройство с MAC {mac} не найдено"
|
||||
if saved_devices[mac].note is None:
|
||||
logger.error(
|
||||
f"Заметка для устройства с MAC {mac} не существует")
|
||||
return False, f"Заметка для устройства с MAC {mac} не существует"
|
||||
saved_devices[mac].note = note
|
||||
self.storage.save_devices(list(saved_devices.values()))
|
||||
logger.info(
|
||||
f"Обновлена заметка '{note}' для устройства с MAC {mac}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка при обновлении заметки для MAC {mac}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
|
||||
def delete_note(self, mac: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Удаляет заметку устройства по MAC-адресу.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении.
|
||||
"""
|
||||
try:
|
||||
saved_devices = self.storage.load_devices()
|
||||
if mac not in saved_devices:
|
||||
logger.warning(f"Устройство с MAC {mac} не найдено")
|
||||
return False, f"Устройство с MAC {mac} не найдено"
|
||||
if saved_devices[mac].note is None:
|
||||
logger.error(
|
||||
f"Заметка для устройства с MAC {mac} не существует")
|
||||
return False, f"Заметка для устройства с MAC {mac} не существует"
|
||||
saved_devices[mac].note = None
|
||||
self.storage.save_devices(list(saved_devices.values()))
|
||||
logger.info(f"Удалена заметка для устройства с MAC {mac}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка при удалении заметки для MAC {mac}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
|
|
@ -41,9 +41,9 @@ class UFWService:
|
|||
print(f"Command error: {result.stderr}")
|
||||
|
||||
if result.returncode != 0:
|
||||
raise subprocess.CalledProcessError(
|
||||
result.returncode, command, result.stdout, result.stderr
|
||||
)
|
||||
raise subprocess.CalledProcessError(result.returncode, command,
|
||||
result.stdout,
|
||||
result.stderr)
|
||||
|
||||
return result.stdout
|
||||
|
||||
|
|
@ -116,9 +116,8 @@ class UFWService:
|
|||
"""
|
||||
try:
|
||||
status_result = self._run_command("ufw status")
|
||||
first_line = (
|
||||
status_result.splitlines()[0] if status_result.splitlines() else ""
|
||||
)
|
||||
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 неактивен"
|
||||
|
|
@ -166,7 +165,9 @@ class UFWService:
|
|||
print(f"Ошибка при выключении UFW: {e}")
|
||||
return False
|
||||
|
||||
def add_rules_base(self, rules: List[UFWRule], direction: str = "") -> bool:
|
||||
def add_rules_base(self,
|
||||
rules: List[UFWRule],
|
||||
direction: str = "") -> bool:
|
||||
"""
|
||||
Базовый метод для добавления правил UFW.
|
||||
|
||||
|
|
@ -296,7 +297,10 @@ class UFWService:
|
|||
except Exception as e:
|
||||
error_msg = f"Ошибка выполнения команды: {str(e)}"
|
||||
print(error_msg)
|
||||
results.append({"success": False, "message": error_msg})
|
||||
results.append({
|
||||
"success": False,
|
||||
"message": error_msg
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Ошибка при добавлении правила: {str(e)}"
|
||||
|
|
@ -334,7 +338,7 @@ class UFWService:
|
|||
|
||||
try:
|
||||
# Извлекаем номер правила из строки [N]
|
||||
rule_num = int(line[1 : line.index("]")])
|
||||
rule_num = int(line[1:line.index("]")])
|
||||
rules.append(rule_num)
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
|
@ -349,32 +353,37 @@ class UFWService:
|
|||
try:
|
||||
# Проверяем существует ли правило с таким номером
|
||||
if rule_number not in rules:
|
||||
results.append(
|
||||
{
|
||||
"number": rule_number,
|
||||
"success": False,
|
||||
"error": f"Правило с номером {rule_number} не найдено",
|
||||
}
|
||||
)
|
||||
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} успешно удалено",
|
||||
}
|
||||
)
|
||||
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)}
|
||||
)
|
||||
print(
|
||||
f"Ошибка при удалении правила {rule_number}: {e}")
|
||||
results.append({
|
||||
"number": rule_number,
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
# Подсчитываем статистику
|
||||
total = len(rule_numbers)
|
||||
|
|
|
|||
392
SRV/services/wifi_service.py
Normal file
392
SRV/services/wifi_service.py
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
import time
|
||||
import logging
|
||||
import subprocess
|
||||
import re
|
||||
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,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WifiStorage:
|
||||
"""Класс для хранения данных о Wi-Fi сетях и их заметках по SSID."""
|
||||
|
||||
def __init__(self, storage_path: str = "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 из файла хранения."""
|
||||
try:
|
||||
if self.storage_file.exists():
|
||||
with open(self.storage_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return {item["ssid"]: item.get("note") for item in data.get("networks", [])}
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при загрузке заметок: {str(e)}")
|
||||
return {}
|
||||
|
||||
def save_notes(self, notes: Dict[str, Optional[str]]) -> None:
|
||||
"""Сохраняет заметки по SSID в файл."""
|
||||
try:
|
||||
data = {"networks": [{"ssid": ssid, "note": note} for ssid, note in notes.items()]}
|
||||
with open(self.storage_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
logger.info(f"Сохранено {len(notes)} заметок в {self.storage_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении заметок: {str(e)}")
|
||||
|
||||
class WifiService:
|
||||
"""Сервис для сканирования текущих Wi-Fi точек доступа"""
|
||||
|
||||
def __init__(self):
|
||||
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-интерфейс"""
|
||||
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():
|
||||
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)
|
||||
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")}
|
||||
|
||||
best_aps_by_ssid: Dict[str, WifiAccessPoint] = {}
|
||||
saved_notes = self.storage.load_notes()
|
||||
|
||||
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}"
|
||||
)
|
||||
|
||||
current_ap = 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,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def add_note(self, ssid: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Добавляет заметку к сети Wi-Fi по SSID.
|
||||
|
||||
Args:
|
||||
ssid (str): SSID сети Wi-Fi.
|
||||
note (str): Текст заметки.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении.
|
||||
"""
|
||||
try:
|
||||
saved_notes = self.storage.load_notes()
|
||||
if ssid not in saved_notes or saved_notes[ssid] is None:
|
||||
logger.info(f"Добавлена новая заметка для сети с SSID {ssid}")
|
||||
elif saved_notes[ssid] is not None:
|
||||
logger.error(f"Заметка для сети с SSID {ssid} уже существует")
|
||||
return False, f"Заметка для сети с SSID {ssid} уже существует"
|
||||
saved_notes[ssid] = note
|
||||
self.storage.save_notes(saved_notes)
|
||||
logger.info(f"Добавлена заметка '{note}' для сети с SSID {ssid}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при добавлении заметки для SSID {ssid}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
|
||||
def update_note(self, ssid: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Обновляет заметку сети Wi-Fi по SSID.
|
||||
|
||||
Args:
|
||||
ssid (str): SSID сети Wi-Fi.
|
||||
note (str): Новый текст заметки.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении.
|
||||
"""
|
||||
try:
|
||||
saved_notes = self.storage.load_notes()
|
||||
if ssid not in saved_notes:
|
||||
logger.warning(f"Сеть с SSID {ssid} не найдена")
|
||||
return False, f"Сеть с SSID {ssid} не найдена"
|
||||
if saved_notes[ssid] is None:
|
||||
logger.error(f"Заметка для сети с SSID {ssid} не существует")
|
||||
return False, f"Заметка для сети с SSID {ssid} не существует"
|
||||
saved_notes[ssid] = note
|
||||
self.storage.save_notes(saved_notes)
|
||||
logger.info(f"Обновлена заметка '{note}' для сети с SSID {ssid}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении заметки для SSID {ssid}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
|
||||
def delete_note(self, ssid: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Удаляет заметку сети Wi-Fi по SSID.
|
||||
|
||||
Args:
|
||||
ssid (str): SSID сети Wi-Fi.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении.
|
||||
"""
|
||||
try:
|
||||
saved_notes = self.storage.load_notes()
|
||||
if ssid not in saved_notes:
|
||||
logger.warning(f"Сеть с SSID {ssid} не найдена")
|
||||
return False, f"Сеть с SSID {ssid} не найдена"
|
||||
if saved_notes[ssid] is None:
|
||||
logger.error(f"Заметка для сети с SSID {ssid} не существует")
|
||||
return False, f"Заметка для сети с SSID {ssid} не существует"
|
||||
saved_notes[ssid] = None
|
||||
self.storage.save_notes(saved_notes)
|
||||
logger.info(f"Удалена заметка для сети с SSID {ssid}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении заметки для SSID {ssid}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue