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

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

9
SRV/.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
.venv
.idea
__pycache__
.gigaide
.vscode
.wheels
.vendor
.python3-deb
storage/devices/

0
SRV/README.md Normal file
View file

0
SRV/__init__.py Normal file
View file

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

12
SRV/api/router.py Normal file
View file

@ -0,0 +1,12 @@
from fastapi import APIRouter
from .v1 import ufw, websocket, ip, network, auth, subnet
router = APIRouter()
router.include_router(ufw.router, prefix="/ufw", tags=["UFW"])
router.include_router(websocket.router, tags=["WebSocket"])
router.include_router(ip.router, prefix="/ip", tags=["IP"])
router.include_router(network.router, prefix="/network", tags=["Network"])
router.include_router(auth.router, prefix="/auth", tags=["Auth"])
router.include_router(subnet.router, prefix="/subnet", tags=["Subnet"])

0
SRV/api/v1/__init__.py Normal file
View file

114
SRV/api/v1/auth.py Normal file
View file

@ -0,0 +1,114 @@
# api/v1/auth.py
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer
from services.auth_service import AuthService
from models.auth import UserCreate, UserLogin, UserResponse, Token
from storage.json_storage import JsonStorage
import logging
import os
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
router = APIRouter()
auth_service = AuthService()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
@router.post("/register", response_model=UserResponse)
async def register(user: UserCreate):
"""Регистрирует нового пользователя"""
try:
db_user = auth_service.register(user)
logger.info(f"Пользователь {user.username} успешно зарегистрирован")
return db_user
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось зарегистрировать пользователя")
@router.post("/login", response_model=Token)
async def login(user: UserLogin, request: Request):
"""Авторизует пользователя и создаёт сессию"""
try:
client_id = request.client.host
logger.debug(f"Попытка авторизации для client_id {client_id}")
result = auth_service.login(user, client_id)
logger.info(f"Успешная авторизация для client_id {client_id}, session_id: {result['session_id']}")
return result
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось авторизовать пользователя")
@router.get("/me")
async def get_current_user(request: Request):
"""Возвращает данные текущего пользователя и токен"""
try:
client_id = request.client.host
logger.debug(f"Запрос /me для client_id {client_id}")
result = auth_service.get_user_by_session(client_id)
return {
"user": result["user"],
"access_token": result["access_token"],
"token_type": "bearer"
}
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось получить данные пользователя")
@router.get("/logout")
async def logout(request: Request):
"""Выход из системы, удаление сессии"""
try:
client_id = request.client.host
logger.debug(f"Запрос logout для client_id {client_id}")
return auth_service.logout(client_id)
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось выйти из системы")
@router.get("/check_json")
async def check_users_file():
"""Проверяет, существует ли файл users.json и содержит ли он пользователей"""
try:
file_path = "/etc/gonets/users.json"
storage = JsonStorage(file_path=file_path)
if not os.path.exists(file_path):
logger.info(f"Файл {file_path} не существует")
return {
"exists": False,
"has_users": False,
"message": "Файл users.json не существует"
}
data = storage._read_data()
has_users = len(data.get("users", [])) > 0
if has_users:
logger.info(f"Файл {file_path} существует и содержит пользователей")
return {
"exists": True,
"has_users": True,
"message": "Файл users.json существует и содержит пользователей"
}
else:
logger.info(f"Файл {file_path} существует, но не содержит пользователей")
return {
"exists": True,
"has_users": False,
"message": "Файл users.json существует, но не содержит пользователей"
}
except Exception as e:
logger.error(f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Ошибка при проверке файла")

449
SRV/api/v1/firewalld.py Normal file
View file

@ -0,0 +1,449 @@
from typing import List
from fastapi import APIRouter, Body, HTTPException, Path, status
from pydantic import BaseModel, Field
from models.firewalld import (
FirewallPort,
FirewallService,
FirewallSource,
FirewallTarget,
FirewallZone,
)
from services.firewalld_service import FirewalldService
router = APIRouter()
firewalld = FirewalldService()
class ErrorResponse(BaseModel):
"""Standard error response model"""
detail: str = Field(..., description="Error description")
class SuccessResponse(BaseModel):
"""Standard success response model"""
message: str = Field(..., description="Success message")
@router.get(
"/zones",
response_model=List[str],
responses={500: {"model": ErrorResponse}},
summary="Get all available zones",
)
async def get_zones() -> List[str]:
"""
Retrieve all available firewalld zones.
Returns:
List[str]: List of zone names
"""
try:
return firewalld.get_zones()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get zones: {str(e)}",
)
@router.get(
"/zones/default",
response_model=str,
responses={500: {"model": ErrorResponse}},
summary="Get default zone",
)
async def get_default_zone() -> str:
"""
Get the current default zone.
Returns:
str: Name of the default zone
"""
try:
return firewalld.get_default_zone()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get default zone: {str(e)}",
)
@router.put(
"/zones/default/{zone}",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Set default zone",
)
async def set_default_zone(
zone: str = Path(..., description="Zone name to set as default"),
) -> SuccessResponse:
"""
Set the default zone.
Args:
zone (str): Name of the zone to set as default
Returns:
SuccessResponse: Success confirmation message
"""
try:
firewalld.set_default_zone(zone)
return SuccessResponse(message=f"Default zone set to {zone}")
except ValueError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to set default zone: {str(e)}",
)
@router.get(
"/zones/{zone}",
response_model=FirewallZone,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Get zone configuration",
)
async def get_zone(zone: str = Path(..., description="Zone name")) -> FirewallZone:
"""
Get detailed configuration for a specific zone.
Args:
zone (str): Name of the zone
Returns:
FirewallZone: Zone configuration details
"""
try:
zone_info = firewalld.get_zone_settings(zone)
return FirewallZone(
name=zone,
target=FirewallTarget.DEFAULT,
interfaces=[],
**zone_info
)
except ValueError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get zone settings: {str(e)}",
)
@router.get(
"/zones/{zone}/services",
response_model=List[str],
responses={500: {"model": ErrorResponse}},
summary="Get services in zone",
)
async def get_services(zone: str = Path(..., description="Zone name")) -> List[str]:
"""
Get all services in a zone.
Args:
zone (str): Name of the zone
Returns:
List[str]: List of service names
"""
try:
return firewalld.get_services(zone)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get services: {str(e)}",
)
@router.post(
"/zones/{zone}/services",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Add service to zone",
)
async def add_service(
zone: str = Path(..., description="Zone name"),
rule: FirewallService = Body(..., description="Service configuration"),
) -> SuccessResponse:
"""
Add a service to a zone.
Args:
zone (str): Name of the zone
rule (FirewallService): Service configuration including service name and optional timeout
Returns:
SuccessResponse: Success confirmation message
"""
try:
firewalld.add_service(zone, rule.name, rule.timeout)
return SuccessResponse(message=f"Service {rule.name} added to zone {zone}")
except ValueError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to add service: {str(e)}",
)
@router.delete(
"/zones/{zone}/services/{service}",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Remove service from zone",
)
async def remove_service(
zone: str = Path(..., description="Zone name"),
service: str = Path(..., description="Service name"),
) -> SuccessResponse:
"""
Remove a service from a zone.
Args:
zone (str): Name of the zone
service (str): Name of the service
Returns:
SuccessResponse: Success confirmation message
"""
try:
firewalld.remove_service(zone, service)
return SuccessResponse(message=f"Service {service} removed from zone {zone}")
except ValueError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' or service '{service}' not found",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to remove service: {str(e)}",
)
@router.get(
"/zones/{zone}/ports",
response_model=List[str],
responses={500: {"model": ErrorResponse}},
summary="Get ports in zone",
)
async def get_ports(zone: str = Path(..., description="Zone name")) -> List[str]:
"""
Get all ports in a zone.
Args:
zone (str): Name of the zone
Returns:
List[str]: List of port configurations
"""
try:
return firewalld.get_ports(zone)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get ports: {str(e)}",
)
@router.post(
"/zones/{zone}/ports",
response_model=SuccessResponse,
responses={
400: {"model": ErrorResponse},
404: {"model": ErrorResponse},
500: {"model": ErrorResponse},
},
summary="Add port to zone",
)
async def add_port(
zone: str = Path(..., description="Zone name"),
rule: FirewallPort = Body(..., description="Port configuration"),
) -> SuccessResponse:
"""
Add a port to a zone.
Args:
zone (str): Name of the zone
rule (FirewallPort): Port configuration including port number, protocol and optional timeout
Returns:
SuccessResponse: Success confirmation message
"""
try:
firewalld.add_port(zone, rule.port, rule.protocol.value, rule.timeout)
return SuccessResponse(
message=f"Port {rule.port}/{rule.protocol.value} added to zone {zone}"
)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to add port: {str(e)}",
)
@router.delete(
"/zones/{zone}/ports",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Remove port from zone",
)
async def remove_port(
zone: str = Path(..., description="Zone name"),
rule: FirewallPort = Body(..., description="Port configuration"),
) -> SuccessResponse:
"""
Remove a port from a zone.
Args:
zone (str): Name of the zone
rule (FirewallPort): Port configuration including port number and protocol
Returns:
SuccessResponse: Success confirmation message
"""
try:
firewalld.remove_port(zone, rule.port, rule.protocol.value)
return SuccessResponse(
message=f"Port {rule.port}/{rule.protocol.value} removed from zone {zone}"
)
except ValueError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to remove port: {str(e)}",
)
@router.get(
"/zones/{zone}/sources",
response_model=List[str],
responses={500: {"model": ErrorResponse}},
summary="Get sources in zone",
)
async def get_sources(zone: str = Path(..., description="Zone name")) -> List[str]:
"""
Get all sources in a zone.
Args:
zone (str): Name of the zone
Returns:
List[str]: List of source addresses
"""
try:
return firewalld.get_sources(zone)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get sources: {str(e)}",
)
@router.post(
"/zones/{zone}/sources",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Add source to zone",
)
async def add_source(
zone: str = Path(..., description="Zone name"),
rule: FirewallSource = Body(..., description="Source configuration"),
) -> SuccessResponse:
"""
Add a source to a zone.
Args:
zone (str): Name of the zone
rule (FirewallSource): Source configuration including source address
Returns:
SuccessResponse: Success confirmation message
"""
try:
firewalld.add_source(zone, rule.address)
return SuccessResponse(message=f"Source {rule.address} added to zone {zone}")
except ValueError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to add source: {str(e)}",
)
@router.delete(
"/zones/{zone}/sources/{source}",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
summary="Remove source from zone",
)
async def remove_source(
zone: str = Path(..., description="Zone name"),
source: str = Path(..., description="Source address"),
) -> SuccessResponse:
"""
Remove a source from a zone.
Args:
zone (str): Name of the zone
source (str): Source address
Returns:
SuccessResponse: Success confirmation message
"""
try:
firewalld.remove_source(zone, source)
return SuccessResponse(message=f"Source {source} removed from zone {zone}")
except ValueError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' or source '{source}' not found",
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to remove source: {str(e)}",
)
@router.post(
"/reload",
response_model=SuccessResponse,
responses={500: {"model": ErrorResponse}},
summary="Reload firewall rules",
)
async def reload_firewall() -> SuccessResponse:
"""
Reload firewall rules.
Returns:
SuccessResponse: Success confirmation message
"""
try:
firewalld.reload()
return SuccessResponse(message="Firewall rules reloaded successfully")
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to reload firewall rules: {str(e)}",
)

37
SRV/api/v1/ip.py Normal file
View file

@ -0,0 +1,37 @@
from typing import Dict, Any
from fastapi import APIRouter, Body, HTTPException
from services.ip_service import IPService
router = APIRouter()
ip_service = IPService()
@router.post("/check_ip")
async def check_ip_config(data: Dict[str, Any] = Body(...)):
""" Проверяет доступность IP-адреса """
try:
result = await ip_service.check_ip_config(data)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
@router.post("/apply_ip")
async def apply_ip_config(data: Dict[str, Any] = Body(...)):
""" Применяет проверенные IP-настройки """
try:
await ip_service.apply_ip_config(data)
return {"success": True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены"}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
@router.post("/save_ip")
async def save_ip_config(data: Dict[str, Any] = Body(...)):
""" Применяет проверенные IP-настройки (Нужен для того, чтобы откатить настройки сетевого профиля обратно) """
try:
await ip_service.apply_ip_config(data)
return {"success": True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены"}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")

128
SRV/api/v1/network.py Normal file
View file

@ -0,0 +1,128 @@
from fastapi import APIRouter, Body, HTTPException
import logging
from typing import Dict, Tuple, List, Union
from services.network_service import NetworkService
from models.network import NetworkInterfacesContainer, Activation, Deactivation, NetworkInterface, WiFiDevice
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
router = APIRouter()
network_service = NetworkService()
class ContainerPositionManager:
"""
Класс для управления позициями и состоянием контейнеров
"""
def __init__(self):
self.positions: Dict[str, Dict[str, Union[int, bool]]] = {
'activation': {'x': 0, 'y': 0, 'isOpen': False},
'deactivation': {'x': 0, 'y': 0, 'isOpen': False}
}
def get_position_and_state(self, container_name: str) -> Tuple[int, int, bool]:
container = self.positions.get(container_name)
if not container:
raise ValueError(f"Неверное имя контейнера: {container_name}")
return container['x'], container['y'], container['isOpen']
def update_position(self, container_id: int, x: int, y: int, isOpen: bool) -> None:
container_name = 'activation' if container_id == 1 else 'deactivation'
self.positions[container_name] = {'x': x, 'y': y, 'isOpen': isOpen}
container_manager = ContainerPositionManager()
async def get_network_data() -> Dict[str, Dict]:
"""
Возвращает текущие сетевые данные в виде словаря
"""
logger.debug("Вызов get_network_data()")
try:
network_data = await network_service.get_network_interfaces_container()
activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces)
activation_x, activation_y, activation_is_open = container_manager.get_position_and_state('activation')
deactivation_x, deactivation_y, deactivation_is_open = container_manager.get_position_and_state('deactivation')
activation = Activation(
id=1,
interfaces=activation_interfaces,
container_x=activation_x,
container_y=activation_y,
isOpen=activation_is_open
)
deactivation = Deactivation(
id=2,
interfaces=deactivation_interfaces,
container_x=deactivation_x,
container_y=deactivation_y,
isOpen=deactivation_is_open
)
return {
"activation": activation.model_dump(),
"deactivation": deactivation.model_dump()
}
except Exception as e:
logger.error(f"Ошибка в get_network_data: {e}")
raise HTTPException(status_code=500, detail="Не удалось получить сетевые данные")
def _split_interfaces(interfaces: List[Union[NetworkInterface, WiFiDevice]]) -> Tuple[
List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]:
"""
Разделяет интерфейсы на активные и неактивные
"""
activation_interfaces = []
deactivation_interfaces = []
for interface in interfaces:
if interface.device_state == '100':
activation_interfaces.append(interface)
else:
deactivation_interfaces.append(interface)
return activation_interfaces, deactivation_interfaces
@router.post("/update_container_position")
async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...)):
"""
Обновляет позицию и состояние контейнера
"""
try:
required_keys = ["container_id", "container_x", "container_y", "isOpen"]
if not all(key in data for key in required_keys):
raise ValueError("Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen")
container_id = data["container_id"]
container_x = data["container_x"]
container_y = data["container_y"]
is_open = data["isOpen"]
# Проверка типов
if not isinstance(container_id, int) or not isinstance(container_x, int) or not isinstance(container_y,
int) or not isinstance(
is_open, bool):
raise ValueError("Неверный тип данных в запросе")
container_manager.update_position(container_id, container_x, container_y, is_open)
return {"success": True, "message": "Позиция и состояние контейнера обновлены"}
except Exception as e:
logger.error(f"Ошибка обновления позиции контейнера: {e}")
raise HTTPException(status_code=400, detail=f"Не удалось обновить положение контейнера: {str(e)}")
@router.get("/get_hostname")
async def get_hostname():
try:
hostname_info = await network_service.get_hostname()
return hostname_info
except Exception as e:
logger.error(f"Ошибка в get_hostname: {e}")
raise HTTPException(status_code=500, detail="Не удалось получить имя хоста")

28
SRV/api/v1/subnet.py Normal file
View file

@ -0,0 +1,28 @@
from fastapi import APIRouter, HTTPException
from typing import List
from models.subnet import SubnetScanResult
from models.wifi import WifiScanResult
from services.subnet_service import SubnetService
from services.wifi_service import WifiService
router = APIRouter()
@router.get("/scan", response_model=SubnetScanResult)
def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None):
"""Сканирует локальную подсеть и возвращает данные об устройствах"""
try:
subnet_service = SubnetService()
result = subnet_service.scan_subnet(subnet, ports)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка сканирования: {str(e)}")
@router.get("/wifi", response_model=WifiScanResult)
def scan_wifi(timeout: int = 30):
"""Сканирует Wi-Fi точки доступа и возвращает данные о них"""
try:
wifi_service = WifiService()
result = wifi_service.scan_wifi(timeout=timeout)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка сканирования Wi-Fi: {str(e)}")

134
SRV/api/v1/ufw.py Normal file
View file

@ -0,0 +1,134 @@
from fastapi import APIRouter, HTTPException
from typing import List
from pydantic import BaseModel, Field
from models.ufw import UFWRulesResponse, UFWStatus, UFWRule
from services.ufw_service import UFWService
router = APIRouter()
class RuleNumbers(BaseModel):
numbers: List[int] = Field(..., description="Список номеров правил для удаления")
@router.get("/rules", response_model=UFWRulesResponse)
async def get_ufw_rules():
"""
Получает все правила UFW с помощью сервиса UFWService и возвращает их в формате JSON.
"""
ufw_service = UFWService()
try:
return ufw_service.get_ufw_rules()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.get("/status", response_model=UFWStatus)
async def get_ufw_status():
"""
Получает текущий статус UFW.
"""
ufw_service = UFWService()
try:
return ufw_service.get_ufw_status()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/enable")
async def enable_ufw():
"""
Включает UFW.
"""
ufw_service = UFWService()
try:
success = ufw_service.enable_ufw()
if success:
return {"status": "success", "message": "UFW успешно включен"}
else:
raise HTTPException(status_code=500, detail="Не удалось включить UFW")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/disable")
async def disable_ufw():
"""
Выключает UFW.
"""
ufw_service = UFWService()
try:
success = ufw_service.disable_ufw()
if success:
return {"status": "success", "message": "UFW успешно выключен"}
else:
raise HTTPException(status_code=500, detail="Не удалось выключить UFW")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/rules/in")
async def add_ufw_in_rules(rules: List[UFWRule]):
"""
Добавляет входящие (in) правила UFW.
"""
ufw_service = UFWService()
try:
# Проверяем статус UFW
status = ufw_service.get_ufw_status()
if not status.enabled:
raise HTTPException(
status_code=400,
detail="UFW отключен. Включите UFW перед добавлением правил."
)
result = ufw_service.add_rules(rules)
if result["success"]:
successful_rules = len([r for r in result["results"] if r["success"]])
total_rules = len(result["results"])
return {
"status": "success",
"message": f"Добавлено {successful_rules} из {total_rules} правил",
"details": result
}
else:
raise HTTPException(status_code=500, detail=result.get("error", "Не удалось добавить правила"))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/rules/out")
async def add_ufw_out_rules(rules: List[UFWRule]):
"""
Добавляет исходящие (out) правила UFW.
"""
ufw_service = UFWService()
try:
# Проверяем статус UFW
status = ufw_service.get_ufw_status()
if not status.enabled:
raise HTTPException(
status_code=400,
detail="UFW отключен. Включите UFW перед добавлением правил."
)
success = ufw_service.add_ufw_out_rules(rules)
if success:
return {"status": "success", "message": "Исходящие правила UFW успешно добавлены"}
else:
raise HTTPException(status_code=500, detail="Не удалось добавить исходящие правила UFW")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.delete("/rules")
async def delete_ufw_rules(rule_numbers: RuleNumbers):
"""
Удаляет правила UFW по их номерам.
"""
ufw_service = UFWService()
try:
result = ufw_service.delete_rules(rule_numbers.numbers)
if result["success"]:
return {
"status": "success",
"message": f"Удалено {result['successful']} из {result['total']} правил",
"details": result
}
else:
raise HTTPException(status_code=500, detail=result["error"])
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")

90
SRV/api/v1/websocket.py Normal file
View file

@ -0,0 +1,90 @@
import asyncio
import logging
from typing import Set
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from starlette.websockets import WebSocketState
from .network import update_container_position, get_network_data
router = APIRouter()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class WebSocketManager:
"""
Класс для управления подключениями к WebSocket-серверу
"""
def __init__(self):
""" Инициализация менеджера подключений к WebSocket-серверу """
self.active_connections: Set[WebSocket] = set()
async def connect(self, websocket: WebSocket):
""" Подключение клиента к WebSocket-серверу """
await websocket.accept()
self.active_connections.add(websocket)
def disconnect(self, websocket: WebSocket):
""" Отключение клиента от WebSocket-серверу """
self.active_connections.remove(websocket)
async def broadcast(self, message: dict):
""" Трансляция сообщений всем подключенным клиентам """
for connection in list(self.active_connections):
try:
if connection.client_state == WebSocketState.CONNECTED:
await connection.send_json(message)
except WebSocketDisconnect:
self.disconnect(connection)
except Exception as e:
logger.error(f"Сообщение об ошибке трансляции: {e}")
self.disconnect(connection)
manager = WebSocketManager()
@router.websocket("/api/v1")
async def websocket_endpoint(websocket: WebSocket):
""" Обработчик WebSocket-соединения """
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_json()
logger.info(f"Полученные данные: {data}")
if all(key in data for key in ["container_id", "container_x", "container_y"]):
await update_container_position(data)
network_data = await get_network_data()
logger.info(f"Текущие сетевые данные: {network_data}")
await manager.broadcast(network_data)
await asyncio.sleep(2)
except WebSocketDisconnect:
logger.info("Клиент отключен")
except Exception as e:
logger.error(f"Ошибка в websocket_endpoint:{e}")
finally:
manager.disconnect(websocket)
if websocket.client_state != WebSocketState.DISCONNECTED:
await websocket.close()
@router.on_event("startup")
async def startup_event():
""" Запуск таска для трансляции сетевых данных """
asyncio.create_task(broadcast_data())
async def broadcast_data():
""" Таск для трансляции сетевых данных """
while True:
try:
network_data = await get_network_data()
if "activation" not in network_data:
logger.error("Ошибка: Ключ 'activation' отсутствует в полученных данных сети")
await asyncio.sleep(2)
continue
await manager.broadcast(network_data)
except Exception as e:
logger.error(f"Ошибка в broadcast_data:{e}")
await asyncio.sleep(2)

29
SRV/config/cors.py Normal file
View file

@ -0,0 +1,29 @@
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
class CORSMiddlewareConfig:
"""
Класс для настройки CORS политики в FastAPI
"""
def __init__(self, app: FastAPI) -> None:
""" Инициализируем приложение FastAPI и список допустимых CORS-оригинов """
self.app = app
self.origins = [
"http://localhost.tiangolo.com",
"https://localhost.tiangolo.com",
"http://localhost",
"http://localhost:8080",
"http://localhost:5174",
"http://localhost:5173",
]
def add_cors_middleware(self) -> None:
""" Добавляем CORS в приложение FastAPI """
self.app.add_middleware(
CORSMiddleware,
allow_origins=self.origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

35
SRV/config/init_config.py Normal file
View file

@ -0,0 +1,35 @@
# init_config.py
import configparser
import os
import secrets
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def init_config():
"""Создаёт конфигурационный файл с SECRET_KEY"""
config_path = "/etc/gonets/config.ini"
config_dir = os.path.dirname(config_path)
if os.path.exists(config_path):
logger.info(f"Конфигурационный файл {config_path} уже существует, пропускаем инициализацию")
return
try:
secret_key = secrets.token_urlsafe(32)
os.makedirs(config_dir, exist_ok=True)
config = configparser.ConfigParser()
config["JWT"] = {
"secret_key": secret_key
}
with open(config_path, "w") as config_file:
config.write(config_file)
os.chmod(config_path, 0o600)
logger.info(f"Создан конфигурационный файл {config_path}")
except Exception as e:
logger.error(f"Ошибка при создании конфигурационного файла: {type(e).__name__} - {str(e)}")
raise
if __name__ == "__main__":
init_config()

107
SRV/config/ports.py Normal file
View file

@ -0,0 +1,107 @@
from typing import Dict, List
class PortConfig:
"""Конфигурация портов для сетевого сканирования."""
_DEFAULT_PORTS: Dict[int, str] = {
20: "FTP (Data)",
21: "FTP (Control)",
22: "SSH",
23: "Telnet",
25: "SMTP",
53: "DNS",
67: "DHCP (Server)",
68: "DHCP (Client)",
69: "TFTP",
80: "HTTP",
110: "POP3",
123: "NTP (Network Time Protocol)",
137: "NetBIOS Name Service",
138: "NetBIOS Datagram Service",
139: "NetBIOS Session Service",
143: "IMAP",
161: "SNMP",
162: "SNMP Trap",
179: "BGP (Border Gateway Protocol)",
443: "HTTPS",
445: "SMB (Microsoft-DS)",
465: "SMTPS",
514: "Syslog (UDP)",
515: "LPD (Printer)",
587: "SMTP Submission",
631: "IPP (Printing)",
993: "IMAPS",
995: "POP3S",
# --- Базы данных ---
1433: "Microsoft SQL Server",
1521: "Oracle DB",
3306: "MySQL/MariaDB",
5432: "PostgreSQL",
6379: "Redis",
27017: "MongoDB",
# --- VPN и удалённый доступ ---
1723: "PPTP VPN",
1701: "L2TP",
500: "IPSec",
4500: "IPSec NAT-T",
1194: "OpenVPN",
3389: "RDP (Remote Desktop Protocol)",
5900: "VNC",
# --- Прокси / Веб ---
1080: "SOCKS Proxy",
8000: "HTTP-Alt",
8080: "HTTP Proxy",
8443: "HTTPS-Alt",
# --- DevOps / Monitoring / Tools ---
9000: "SonarQube / PHP-FPM",
9100: "Prometheus Node Exporter",
9200: "Elasticsearch HTTP",
9300: "Elasticsearch Cluster",
11211: "Memcached",
# --- Прочее ---
6667: "IRC",
4000: "ICQ / Custom apps",
25565: "Minecraft Server",
53: "DNS (UDP & TCP)",
}
def __init__(self, custom_ports: Dict[int, str] = None):
"""
Инициализирует конфигурацию портов.
Args:
custom_ports: Пользовательский словарь портов для добавления или переопределения.
"""
self._ports = self._DEFAULT_PORTS.copy()
if custom_ports:
self._ports.update(custom_ports)
def get_port_name(self, port: int) -> str:
"""
Возвращает название сервиса для указанного порта.
Args:
port: Номер порта.
Returns:
Название сервиса или строка вида 'Port-<port>'.
"""
return self._ports.get(port, f"Port-{port}")
def get_ports(self) -> List[int]:
"""
Возвращает список всех номеров портов.
Returns:
Список номеров портов.
"""
return list(self._ports.keys())
port_config = PortConfig()

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

25
SRV/core/cli_executor.py Normal file
View file

@ -0,0 +1,25 @@
import subprocess
from typing import Tuple
class CLIExecutor:
"""
Класс для выполнения CLI-команд
"""
@staticmethod
def run(command: list) -> Tuple[int, str]:
"""
Выполняет команду в системе и возвращает код и вывод.
Args:
command (list): Список аргументов команды для выполнения.
Returns:
tuple: Код возврата и вывод команды.
"""
result = subprocess.run(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=False # Не вызываем исключение при ошибке
)
return result.returncode, result.stdout.decode('utf-8')

276
SRV/docs/firewalld.md Normal file
View file

@ -0,0 +1,276 @@
# Документация API FirewallD
## Общее описание
API предоставляет интерфейс для управления межсетевым экраном FirewallD в Linux системах. API построено на основе взаимодействия с FirewallD через D-Bus интерфейс.
## Внутренняя архитектура
### 1. Уровни взаимодействия
```
[HTTP API] -> [FastAPI Router] -> [FirewallD Service] -> [D-Bus] -> [FirewallD]
```
### 2. Компоненты системы
#### 2.1 D-Bus интерфейс
FirewallD предоставляет D-Bus интерфейс `org.fedoraproject.FirewallD1` для управления. Наш сервис подключается к системной шине D-Bus и взаимодействует с FirewallD через этот интерфейс.
Основные методы D-Bus:
- `getZones()` - получение списка зон
- `getDefaultZone()` - получение зоны по умолчанию
- `addService()` - добавление сервиса
- `removeService()` - удаление сервиса
и т.д.
#### 2.2 Модели данных
В `models/firewalld.py` определены Pydantic модели для валидации данных:
##### FirewallZone
Используется для представления зоны файрвола:
```python
class FirewallZone(BaseModel):
name: str
target: Optional[str] = None
services: List[str] = Field(default_factory=list)
ports: List[str] = Field(default_factory=list)
sources: List[str] = Field(default_factory=list)
interfaces: List[str] = Field(default_factory=list)
```
Применяется при получении настроек зоны через API.
##### FirewallService
Модель для конфигурации сервиса:
```python
class FirewallService(BaseModel):
name: str
ports: List[str] = Field(default_factory=list)
protocols: List[str] = Field(default_factory=list)
modules: List[str] = Field(default_factory=list)
```
Используется при добавлении новых сервисов через API.
##### FirewallPort
Модель для конфигурации порта с валидацией протокола:
```python
class FirewallPort(BaseModel):
port: int
protocol: Optional[str] = Field(default=None)
timeout: Optional[int] = 0
```
Применяется в API endpoint POST /api/v1/firewalld/zones/{zone}/ports для валидации входных данных.
##### FirewallSource
Модель для конфигурации источника с валидацией IP-адреса:
```python
class FirewallSource(BaseModel):
address: str = Field(..., pattern=r'^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?:/[0-9]{1,2})?$')
zone: Optional[str] = None
```
Используется в API endpoint POST /api/v1/firewalld/zones/{zone}/sources для валидации IP-адресов.
##### FirewallRule
Обобщенная модель правила файрвола:
```python
class FirewallRule(BaseModel):
zone: str
source: Optional[FirewallSource] = None
port: Optional[FirewallPort] = None
service: Optional[str] = None
```
Используется для комплексных операций с правилами.
В API (`api/v1/firewalld.py`) определены дополнительные модели для запросов:
##### PortRule
```python
class PortRule(BaseModel):
port: int
protocol: str
timeout: Optional[int] = 0
```
Используется для валидации входных данных при добавлении портов.
##### ServiceRule
```python
class ServiceRule(BaseModel):
service: str
timeout: Optional[int] = 0
```
Используется для валидации входных данных при добавлении сервисов.
##### SourceRule
```python
class SourceRule(BaseModel):
source: str
```
Используется для валидации входных данных при добавлении источников.
Все модели используют Pydantic для:
- Валидации входных данных
- Автоматической конвертации типов
- Генерации OpenAPI документации
- Проверки бизнес-правил (например, валидация протокола в FirewallPort)
#### 2.3 Сервисный слой
`FirewallService` в `services/firewalld_service.py` обеспечивает:
- Подключение к D-Bus
- Обработку ошибок
- Преобразование данных между API и D-Bus форматами
## API Endpoints
### Управление зонами
#### GET /api/v1/firewalld/zones
Получение списка всех доступных зон.
**Ответ:**
```json
[
"public",
"internal",
"external",
...
]
```
**Внутренняя работа:**
1. Запрос поступает в FastAPI router
2. Router вызывает `FirewallService.get_zones()`
3. Сервис делает вызов `getZones()` через D-Bus
4. Результат преобразуется в JSON и возвращается клиенту
#### GET /api/v1/firewalld/zones/default
Получение зоны по умолчанию.
**Ответ:**
```json
{
"name": "public"
}
```
**Внутренняя работа:**
1. Вызов `FirewallService.get_default_zone()`
2. D-Bus запрос `getDefaultZone()`
3. Преобразование результата
#### PUT /api/v1/firewalld/zones/default/{zone}
Установка зоны по умолчанию.
**Параметры:**
- `zone`: имя зоны
**Внутренняя работа:**
1. Валидация имени зоны
2. Вызов `FirewallService.set_default_zone()`
3. D-Bus запрос `setDefaultZone()`
### Управление сервисами
#### GET /api/v1/firewalld/zones/{zone}/services
Получение списка сервисов в зоне.
**Ответ:**
```json
{
"services": ["http", "https", "ssh"]
}
```
**Внутренняя работа:**
1. Проверка существования зоны
2. Вызов `FirewallService.get_services()`
3. D-Bus запрос `getServices()`
#### POST /api/v1/firewalld/zones/{zone}/services
Добавление сервиса в зону.
**Тело запроса:**
```json
{
"service": "http",
"timeout": 0
}
```
**Внутренняя работа:**
1. Валидация данных через Pydantic модель
2. Вызов `FirewallService.add_service()`
3. D-Bus запрос `addService()`
#### DELETE /api/v1/firewalld/zones/{zone}/services/{service}
Удаление сервиса из зоны.
**Внутренняя работа:**
1. Проверка существования сервиса
2. Вызов `FirewallService.remove_service()`
3. D-Bus запрос `removeService()`
### Управление портами
#### GET /api/v1/firewalld/zones/{zone}/ports
Получение списка портов в зоне.
**Ответ:**
```json
{
"ports": ["80/tcp", "443/tcp"]
}
```
#### POST /api/v1/firewalld/zones/{zone}/ports
Добавление порта в зону.
**Тело запроса:**
```json
{
"port": 80,
"protocol": "tcp",
"timeout": 0
}
```
#### DELETE /api/v1/firewalld/zones/{zone}/ports
Удаление порта из зоны.
### Управление источниками
#### GET /api/v1/firewalld/zones/{zone}/sources
Получение списка источников в зоне.
#### POST /api/v1/firewalld/zones/{zone}/sources
Добавление источника в зону.
#### DELETE /api/v1/firewalld/zones/{zone}/sources/{source}
Удаление источника из зоны.
## Обработка ошибок
API возвращает следующие коды ошибок:
- 400: Неверный запрос (например, неверный формат данных)
- 404: Ресурс не найден (зона, сервис и т.д.)
- 500: Внутренняя ошибка сервера
При ошибке D-Bus или FirewallD, ошибка логируется и возвращается соответствующий HTTP код.
## Возможные улучшения
1. **Безопасность:**
- Добавить аутентификацию
- Добавить rate limiting
- Логирование действий
2. **Функциональность:**
- Добавить поддержку rich rules
- Управление ICMP правилами
- Управление forward портами
3. **Производительность:**
- Кэширование часто запрашиваемых данных
- Batch операции для множественных изменений
4. **Надёжность:**
- Retry механизм для D-Bus операций
- Более детальная обработка ошибок
- Валидация состояния FirewallD

0
SRV/docs/network.md Normal file
View file

208
SRV/docs/ufw.md Normal file
View file

@ -0,0 +1,208 @@
# UFW API Documentation
## Overview
API для управления файрволом UFW (Uncomplicated Firewall) в Linux. Позволяет добавлять, удалять и просматривать правила межсетевого экрана.
## Архитектура
### Модели данных
#### Protocol (Протокол)
```python
class Protocol(str, Enum):
TCP = "tcp" # Только TCP протокол
UDP = "udp" # Только UDP протокол
BOTH = "both" # Оба протокола (TCP и UDP)
```
При указании `BOTH`, система автоматически создает правила для обоих протоколов, используя `proto tcp/udp` в команде UFW.
#### Action (Действие)
```python
class Action(str, Enum):
ALLOW = "allow" # Разрешить трафик
DENY = "deny" # Запретить трафик без уведомления
REJECT = "reject" # Запретить трафик с уведомлением отправителю
```
#### Direction (Направление)
```python
class Direction(str, Enum):
IN = "in" # Входящий трафик
OUT = "out" # Исходящий трафик
```
#### UFWRule (Правило UFW)
```python
class UFWRule(BaseModel):
action: Action # Действие (allow/deny/reject)
direction: Direction # Направление трафика (in/out)
from_ip: str # IP-адрес источника
to_ip: str # IP-адрес назначения
port: Union[int, str] # Порт или диапазон портов
protocol: Protocol # Протокол
comment: Optional[str] # Комментарий к правилу
```
Модель выполняет следующие проверки:
1. Валидация IP адресов:
- Проверяет корректность IPv4/IPv6 адресов
- Поддерживает CIDR нотацию
- Проверяет совместимость версий IP (IPv4/IPv6)
2. Валидация портов:
- Одиночный порт: число от 1 до 65535
- Диапазон портов: строка вида "3000:4000"
- Проверяет корректность диапазона (start <= end)
## API Endpoints
### Получение правил
#### GET /ufw/rules
Получает список всех правил UFW.
**Response:**
```json
{
"enabled": true,
"rules": [
{
"action": "allow",
"direction": "in",
"from_ip": "0.0.0.0/0",
"to_ip": "0.0.0.0/0",
"port": 80,
"protocol": "tcp",
"comment": "Allow HTTP"
}
]
}
```
### Добавление правил
#### POST /ufw/rules/in
Добавляет входящие правила UFW.
**Request:**
```json
{
"rules": [
{
"action": "allow",
"from_ip": "0.0.0.0/0",
"to_ip": "0.0.0.0/0",
"port": 80,
"protocol": "tcp",
"comment": "Allow HTTP"
}
]
}
```
**Response:**
```json
{
"status": "success",
"message": "Входящие правила UFW успешно добавлены"
}
```
#### POST /ufw/rules/out
Добавляет исходящие правила UFW.
**Request:**
```json
{
"rules": [
{
"action": "allow",
"from_ip": "192.168.1.0/24",
"to_ip": "0.0.0.0/0",
"port": 443,
"protocol": "tcp",
"comment": "Allow HTTPS outbound"
}
]
}
```
**Response:**
```json
{
"status": "success",
"message": "Исходящие правила UFW успешно добавлены"
}
```
### Удаление правил
#### DELETE /ufw/rules
Удаляет правила UFW по их номерам.
**Request:**
```json
{
"numbers": [1, 2, 3]
}
```
**Response:**
```json
{
"status": "success",
"message": "Правила UFW успешно удалены"
}
```
### Управление состоянием
#### POST /ufw/enable
Включает UFW.
**Response:**
```json
{
"status": "success",
"message": "UFW успешно включен"
}
```
#### POST /ufw/disable
Выключает UFW.
**Response:**
```json
{
"status": "success",
"message": "UFW успешно выключен"
}
```
## Обработка ошибок
### HTTP Status Codes
1. Успешные операции (200):
- Получение списка правил
- Успешное добавление/удаление правил
- Включение/выключение UFW
2. Ошибки клиента (400):
- UFW отключен при попытке добавления правил
- Некорректный IP адрес или CIDR
- Неверный номер порта или диапазон
- Несовместимые версии IP (IPv4/IPv6)
- Неверные значения enum (action/protocol/direction)
3. Ошибки сервера (500):
- Ошибки выполнения команд UFW
- Внутренние ошибки сервера
### Формат ошибок
```json
{
"detail": "Описание ошибки"
}

14
SRV/gonets.service Executable file
View file

@ -0,0 +1,14 @@
[Unit]
Description=GONETS FastAPI Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/usr/share/gonets
ExecStart=/usr/bin/python3 /usr/share/gonets/main.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

22
SRV/main.py Normal file
View file

@ -0,0 +1,22 @@
from fastapi import FastAPI
import uvicorn
from api.router import router
from config.cors import CORSMiddlewareConfig
from services.network_service import NetworkService
app = FastAPI(
title="GONETS",
description="Панель для управления системными сервисами Linux",
version="0.0.2",
)
cors_config = CORSMiddlewareConfig(app)
cors_config.add_cors_middleware()
app.include_router(router)
network_service = NetworkService()
if __name__ == "__main__":
#uvicorn.run("main:app", host='0.0.0.0', port=8001, reload=True, ssl_keyfile="/etc/gonets/certs/gonets.key", ssl_certfile="/etc/gonets/certs/gonets.crt")
uvicorn.run("main:app", host='0.0.0.0', port=8001, reload=True)

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

23
SRV/models/auth.py Normal file
View file

@ -0,0 +1,23 @@
# models.py
from pydantic import BaseModel
from typing import Optional
class UserCreate(BaseModel):
username: str
password: str
is_admin: bool = False
class UserLogin(BaseModel):
username: str
password: str
class UserResponse(BaseModel):
id: int
username: str
is_admin: bool
class Token(BaseModel):
access_token: str
token_type: str
session_id: Optional[str] = None
user: Optional[UserResponse] = None

115
SRV/models/firewalld.py Normal file
View file

@ -0,0 +1,115 @@
from enum import Enum
from typing import Annotated, List, Optional
from pydantic import BaseModel, Field, field_validator
from pydantic.types import StringConstraints
class FirewallTarget(str, Enum):
"""Firewall zone target types"""
ACCEPT = "ACCEPT"
REJECT = "REJECT"
DROP = "DROP"
DEFAULT = "default"
class Protocol(str, Enum):
"""Network protocols supported by firewalld"""
TCP = "tcp"
UDP = "udp"
class FirewallZone(BaseModel):
"""Represents a firewalld zone configuration"""
name: Annotated[str, StringConstraints(min_length=1)] = Field(
..., description="Name of the zone"
)
target: Optional[FirewallTarget] = Field(
default=FirewallTarget.DEFAULT, description="Zone target type"
)
services: List[str] = Field(
default_factory=list, description="List of enabled services"
)
ports: List[str] = Field(default_factory=list, description="List of enabled ports")
sources: List[str] = Field(
default_factory=list, description="List of source addresses"
)
interfaces: List[str] = Field(
default_factory=list, description="List of network interfaces"
)
class FirewallService(BaseModel):
"""Represents a firewalld service configuration"""
name: Annotated[str, StringConstraints(min_length=1)] = Field(
..., description="Name of the service"
)
ports: List[str] = Field(
default_factory=list, description="List of ports used by the service"
)
protocols: List[str] = Field(
default_factory=list, description="List of protocols used by the service"
)
modules: List[str] = Field(
default_factory=list,
description="List of kernel modules required by the service",
)
timeout: Optional[int] = Field(
default=0, ge=0, description="Timeout in seconds (0 for permanent)"
)
class FirewallPort(BaseModel):
"""Represents a firewalld port configuration"""
port: int = Field(..., ge=1, le=65535, description="Port number (1-65535)")
protocol: Protocol = Field(..., description="Network protocol (tcp/udp)")
timeout: Optional[int] = Field(
default=0, ge=0, description="Timeout in seconds (0 for permanent)"
)
def __str__(self) -> str:
return f"{self.port}/{self.protocol.value}"
class FirewallSource(BaseModel):
"""Represents a firewalld source configuration"""
address: Annotated[
str,
StringConstraints(pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?:/[0-9]{1,2})?$"),
] = Field(..., description="IP address or network in CIDR notation")
zone: Optional[str] = Field(default=None, description="Zone name for the source")
@field_validator("address")
def validate_ip_address(cls, v):
# Validate IP address octets
ip = v.split("/")[0]
octets = ip.split(".")
if not all(0 <= int(octet) <= 255 for octet in octets):
raise ValueError("Invalid IP address octets")
# Validate CIDR if present
if "/" in v:
cidr = int(v.split("/")[1])
if not 0 <= cidr <= 32:
raise ValueError("CIDR must be between 0 and 32")
return v
class FirewallRule(BaseModel):
"""Represents a complete firewalld rule"""
zone: str = Field(..., description="Zone name")
source: Optional[FirewallSource] = Field(
default=None, description="Source configuration"
)
port: Optional[FirewallPort] = Field(default=None, description="Port configuration")
service: Optional[str] = Field(default=None, description="Service name")
timeout: Optional[int] = Field(
default=0, ge=0, description="Rule timeout in seconds"
)

70
SRV/models/network.py Normal file
View file

@ -0,0 +1,70 @@
from typing import List, Optional, Union
from pydantic import BaseModel
# Класс для маршрутов IPv4
class IPv4Route(BaseModel):
destination: str
prefix: int
next_hop: str
metric: int
# Класс для маршрутов IPv6
class IPv6Route(BaseModel):
destination: str
prefix: int
next_hop: str
metric: int
# Класс для индивидуальной конфигурации IP
class IndividualIPConfig(BaseModel):
address: str
routes: List[Union[IPv4Route, IPv6Route]]
method: Optional[str]
gateway: str
dns: List[str]
# Класс для конфигурации IP
class IPConfig(BaseModel):
configurations: List[IndividualIPConfig]
# Класс для сетевого интерфейса
class NetworkInterface(BaseModel):
name: str
type: str
mac_address: str
device_state: str
active_connection: str
device_mtu: str
ipv4: IPConfig
ipv6: IPConfig
container_x: int = 0
container_y: int = 0
# Класс для WiFi устройства
class WiFiDevice(NetworkInterface):
ssid: Optional[str] = None
bssid: Optional[str] = None
bitrate: Optional[int] = None
strength: Optional[int] = None
frequency: Optional[float] = None
mode: Union[str, int] = None
class NetworkInterfacesContainer(BaseModel):
interfaces: List[Union[NetworkInterface, WiFiDevice]]
container_x: int
container_y: int
class Activation(BaseModel):
id: int
interfaces: List[Union[NetworkInterface, WiFiDevice]]
container_x: int
container_y: int
isOpen: bool = True
class Deactivation(BaseModel):
id: int
interfaces: List[Union[NetworkInterface, WiFiDevice]]
container_x: int
container_y: int
isOpen: bool = True

14
SRV/models/subnet.py Normal file
View file

@ -0,0 +1,14 @@
from pydantic import BaseModel
from typing import Optional, List
class Device(BaseModel):
ip: str
mac: Optional[str] = None
hostname: Optional[str] = None
vendor: Optional[str] = None
open_ports: Optional[List[str]] = None
status: str = "online"
last_seen: Optional[str] = None
class SubnetScanResult(BaseModel):
devices: List[Device]

263
SRV/models/ufw.py Normal file
View file

@ -0,0 +1,263 @@
from typing import List, Optional, Annotated, Union
from enum import Enum
from pydantic import BaseModel, Field, validator
import ipaddress
class Protocol(str, Enum):
"""
Enum для сетевых протоколов UFW.
"""
TCP = "tcp"
UDP = "udp"
BOTH = "both"
@property
def protocols(self) -> List[str]:
"""Возвращает список протоколов для использования в команде UFW."""
return ["tcp", "udp"] if self == Protocol.BOTH else [self.value]
class Action(str, Enum):
"""
Enum для действий UFW.
Attributes:
ALLOW: Разрешить трафик
DENY: Запретить трафик без уведомления
REJECT: Запретить трафик с уведомлением отправителю
"""
ALLOW = "allow"
DENY = "deny"
REJECT = "reject"
class Direction(str, Enum):
"""
Enum для направления трафика UFW.
Attributes:
IN: Входящий трафик
OUT: Исходящий трафик
"""
IN = "in"
OUT = "out"
class UFWRule(BaseModel):
"""Модель правила UFW"""
action: Action
direction: Direction
from_ip: str
to_ip: str
port: Union[int, str]
protocol: Protocol
comment: Optional[str] = None
@validator("port")
def validate_port(cls, v):
"""
Проверяет корректность порта или диапазона портов.
Args:
v: Порт (число) или диапазон портов (строка вида "3000:3999")
Returns:
Union[int, str]: Валидированное значение порта
Raises:
ValueError: Если порт или диапазон портов некорректен
"""
if isinstance(v, int):
if not (1 <= v <= 65535):
raise ValueError(f"Порт должен быть в диапазоне 1-65535, получено: {v}")
return v
elif isinstance(v, str):
try:
start, end = map(int, v.split(':'))
if not (1 <= start <= end <= 65535):
raise ValueError
return v
except (ValueError, TypeError):
raise ValueError(f"Некорректный диапазон портов: {v}. Формат: start:end, где 1 <= start <= end <= 65535")
else:
raise ValueError(f"Порт должен быть числом или строкой с диапазоном, получено: {type(v)}")
@validator("from_ip", "to_ip")
def validate_ip(cls, v):
"""Проверяет корректность IP адреса"""
try:
# Пробуем создать объект IP-сети, что автоматически проверит корректность адреса
ipaddress.ip_network(v, strict=False)
return v
except ValueError:
raise ValueError(f"Некорректный IP адрес или подсеть: {v}")
@validator("to_ip")
def validate_ip_version_compatibility(cls, v, values):
"""Проверяет совместимость версий IP адресов"""
if "from_ip" not in values:
return v
# Определяем версии IP
from_ip = ipaddress.ip_network(values["from_ip"], strict=False)
to_ip = ipaddress.ip_network(v, strict=False)
if isinstance(from_ip, ipaddress.IPv6Network) != isinstance(to_ip, ipaddress.IPv6Network):
raise ValueError("Несовместимые версии IP: нельзя смешивать IPv4 и IPv6 в одном правиле")
return v
@validator("action", pre=True)
def validate_action(cls, v):
"""Приводит значение action к нижнему регистру перед валидацией"""
if isinstance(v, str):
return v.lower()
return v
@validator("protocol", pre=True)
def validate_protocol(cls, v):
"""Приводит значение protocol к нижнему регистру перед валидацией"""
if isinstance(v, str):
return v.lower()
return v
def to_ufw_command(self) -> str:
"""Преобразует правило в команду UFW."""
cmd_parts = ["ufw"]
# Добавляем действие
cmd_parts.append(self.action.value)
# Добавляем направление
cmd_parts.append(self.direction.value)
# Добавляем from с IP
cmd_parts.extend(["from", self.from_ip])
# Добавляем to с IP
cmd_parts.extend(["to", self.to_ip])
# Добавляем порт и протокол
port_str = str(self.port) # Работает как с числами, так и с диапазонами
cmd_parts.extend(["port", port_str])
# Добавляем протокол
proto = self.protocol.value
if proto == "both":
cmd_parts.append("proto tcp/udp")
else:
cmd_parts.extend(["proto", proto])
# Добавляем комментарий если есть
if self.comment:
cmd_parts.extend(["comment", f"'{self.comment}'"])
return " ".join(cmd_parts)
@classmethod
def from_ufw_output(cls, line: str) -> "UFWRule":
"""
Создает экземпляр UFWRule из строки вывода команды 'ufw status numbered'.
Args:
line: Строка вывода UFW (например, "[1] 80/tcp ALLOW IN 192.168.1.0/24 # Web Server")
Returns:
UFWRule: Новый экземпляр правила
"""
import re
# Регулярное выражение для разбора строки правила UFW
pattern = r"""^\s*
\[\s*(?P<number>\d+)\s*\]\s*
(?P<port>\d+(?::\d+)?)/(?P<protocol>tcp(?:/udp)?|udp)\s+
(?P<action>ALLOW|DENY|REJECT)\s+(?P<direction>IN|OUT)\s+
(?P<from_ip>[\w\d\./]+|\bAnywhere\b)
(?:\s+\(v6\))?
(?:\s+\((?:out|in)\))?
\s*
(?:\#\s*(?P<comment>.*?))?
\s*$
"""
match = re.match(pattern, line, re.VERBOSE | re.IGNORECASE)
if not match:
raise ValueError(f"Невозможно разобрать строку правила UFW: {line}")
groups = match.groupdict()
# Преобразуем протокол в enum
proto_str = groups['protocol'].lower()
protocol = Protocol.BOTH if proto_str == "tcp/udp" else \
Protocol.UDP if proto_str == "udp" else Protocol.TCP
# Преобразуем действие в enum (игнорируем регистр)
action_str = groups['action'].lower()
action = Action.ALLOW if action_str == "allow" else \
Action.DENY if action_str == "deny" else Action.REJECT
# Преобразуем направление в enum
direction_str = groups['direction'].lower()
direction = Direction.IN if direction_str == "in" else Direction.OUT
# Обрабатываем IP адреса
from_ip = groups['from_ip'].strip()
if from_ip.lower() == "anywhere":
from_ip = "0.0.0.0/0"
# Для исходящих правил меняем местами from и to
to_ip = "0.0.0.0/0" # По умолчанию anywhere
# Обрабатываем порт
port = groups['port']
if ':' in port:
# Оставляем как строку для диапазона
port = port.strip()
else:
# Преобразуем в число для одиночного порта
port = int(port)
# Обрабатываем комментарий
comment = groups.get('comment', '').strip() if groups.get('comment') else None
return cls(
action=action,
direction=direction,
from_ip=from_ip,
to_ip=to_ip,
port=port,
protocol=protocol,
comment=comment
)
class UFWRulesResponse(BaseModel):
"""
Модель для представления списка правил UFW.
Attributes:
enabled: Включен ли UFW
rules: Список правил UFW
total_rules: Общее количество правил
"""
enabled: bool = Field(..., description="Включен ли UFW")
rules: List[UFWRule] = Field(default_factory=list, description="Список правил UFW")
@property
def total_rules(self) -> int:
"""Возвращает общее количество правил."""
return len(self.rules)
class UFWStatus(BaseModel):
"""
Модель для представления статуса UFW.
Attributes:
enabled: Включен ли UFW
message: Сообщение о статусе
last_checked: Время последней проверки статуса
"""
enabled: bool = Field(..., description="Включен ли UFW")
message: str = Field(..., description="Сообщение о статусе")

15
SRV/models/wifi.py Normal file
View file

@ -0,0 +1,15 @@
from pydantic import BaseModel
from typing import Optional, List
class WifiAccessPoint(BaseModel):
ssid: Optional[str] = None
bssid: str
channel: Optional[int] = None
signal: Optional[int] = None
encryption: Optional[str] = None
frequency: Optional[str] = None
vendor: Optional[str] = None
hidden: bool = False
class WifiScanResult(BaseModel):
access_points: List[WifiAccessPoint]

2240
SRV/poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

45
SRV/pyproject.toml Normal file
View file

@ -0,0 +1,45 @@
[tool.poetry]
name = "GONETS"
version = "0.1.0"
description = "API для управления системными сервисами Linux"
authors = ["Sergey Galanskiy <merelden.77@gmail.com>", "Vladimir Shalimow <vova@texnico.ru>"]
readme = "README.md"
packages = [
{include = "api"},
{include = "config"},
{include = "models"},
{include = "services"},
{include = "core"},
{include = "storage"},
]
[tool.poetry.dependencies]
python = "^3.11"
pydbus = "^0.6.0"
fastapi = {extras = ["standard"], version = "^0.115.11"}
pydantic = "^2.10.3"
pygobject = "^3.50.0"
sdbus-networkmanager = "^2.0.0"
uvicorn = "^0.32.1"
websockets = "^14.1"
uuid = "^1.30"
scapy = "^2.6.1"
netifaces = "^0.11.0"
netaddr = "^1.3.0"
dbus-python = "^1.3.2"
python-pam = "^2.0.2"
six = "^1.17.0"
python-jose = {extras = ["cryptography"], version = "^3.4.0"}
storage = "^0.0.4.3"
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
python-multipart = "^0.0.20"
bcrypt = "^4.3.0"
mac-vendor-lookup = "^0.1.12"
pywifi = "^1.1.12"
manuf = "^1.1.5"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

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)

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

101
SRV/storage/json_storage.py Normal file
View file

@ -0,0 +1,101 @@
# storage/json_storage.py
import json
import os
import logging
from passlib.context import CryptContext
from typing import Optional
from models.auth import UserCreate, UserResponse
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class JsonStorage:
"""Хранилище пользователей в JSON-файле"""
def __init__(self, file_path: str = "/etc/gonets/users.json"):
self.file_path = file_path
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
self._ensure_file_exists()
def _ensure_file_exists(self):
"""Создаёт JSON-файл, если он не существует"""
try:
logger.info(f"Проверка существования файла {self.file_path}")
if not os.path.exists(self.file_path):
logger.info(f"Создание директории {os.path.dirname(self.file_path)}")
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
logger.info(f"Создание файла {self.file_path}")
with open(self.file_path, "w") as f:
json.dump({"users": [], "next_id": 1}, f)
os.chmod(self.file_path, 0o600)
logger.info(f"Создан файл {self.file_path}")
else:
logger.info(f"Файл {self.file_path} уже существует")
except Exception as e:
logger.error(f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}")
raise
def _read_data(self) -> dict:
"""Читает данные из JSON-файла"""
try:
with open(self.file_path, "r") as f:
return json.load(f)
except FileNotFoundError:
logger.info(f"Файл {self.file_path} не существует, возвращаем пустую структуру")
return {"users": [], "next_id": 1}
except Exception as e:
logger.error(f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}")
raise
def _write_data(self, data: dict):
"""Записывает данные в JSON файл"""
try:
with open(self.file_path, "w") as f:
json.dump(data, f, indent=2)
except Exception as e:
logger.error(f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}")
raise
def create_user(self, user: UserCreate) -> UserResponse:
"""Создаёт нового пользователя"""
try:
data = self._read_data()
users = data["users"]
next_id = data["next_id"]
if any(u["username"] == user.username for u in users):
logger.error(f"Пользователь {user.username} уже существует")
raise ValueError("Пользователь уже существует")
password_hash = self.pwd_context.hash(user.password)
new_user = {
"id": next_id,
"username": user.username,
"password_hash": password_hash,
"is_admin": user.is_admin
}
users.append(new_user)
data["next_id"] = next_id + 1
self._write_data(data)
logger.info(f"Пользователь {user.username} успешно зарегистрирован")
return UserResponse(id=new_user["id"], username=new_user["username"], is_admin=new_user["is_admin"])
except Exception as e:
logger.error(f"Ошибка при создании пользователя {user.username}: {type(e).__name__} - {str(e)}")
raise
def get_user_by_username(self, username: str) -> Optional[dict]:
"""Получает пользователя по имени"""
try:
if not os.path.exists(self.file_path):
logger.info(f"Файл {self.file_path} не существует, пользователь {username} не найден")
return None
data = self._read_data()
for user in data["users"]:
if user["username"] == username:
return user
return None
except Exception as e:
logger.error(f"Ошибка при получении пользователя {username}: {type(e).__name__} - {str(e)}")
raise
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
"""Проверяет пароль"""
return self.pwd_context.verify(plain_password, hashed_password)

View file

@ -0,0 +1,141 @@
# storage/session_storage.py
import json
import os
import uuid
import logging
from typing import Optional, Dict
from datetime import datetime, timedelta
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SessionStorage:
def __init__(self, file_path: str = "/etc/gonets/sessions.json"):
self.file_path = file_path
self._ensure_file_exists()
def _ensure_file_exists(self):
try:
logger.info(f"Проверка существования файла {self.file_path}")
if not os.path.exists(self.file_path):
logger.info(f"Создание директории {os.path.dirname(self.file_path)}")
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
logger.info(f"Создание файла {self.file_path}")
with open(self.file_path, "w") as f:
json.dump({"sessions": {}}, f)
os.chmod(self.file_path, 0o600)
logger.info(f"Создан файл {self.file_path}")
else:
logger.info(f"Файл {self.file_path} уже существует")
except Exception as e:
logger.error(f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}")
raise
def _read_data(self) -> Dict:
try:
with open(self.file_path, "r") as f:
return json.load(f)
except Exception as e:
logger.error(f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}")
raise
def _write_data(self, data: Dict):
try:
with open(self.file_path, "w") as f:
json.dump(data, f, indent=2)
except Exception as e:
logger.error(f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}")
raise
def create_session(self, username: str, token: str, client_id: str) -> str:
"""Создаёт новую сессию и возвращает session_id"""
try:
self.clean_expired_sessions()
data = self._read_data()
session_id = str(uuid.uuid4())
created_at = datetime.utcnow().isoformat()
expiry = (datetime.utcnow() + timedelta(minutes=30)).isoformat()
data["sessions"][session_id] = {
"username": username,
"token": token,
"client_id": client_id,
"created_at": created_at,
"expiry": expiry
}
self._write_data(data)
logger.info(f"Сессия создана для пользователя {username} с session_id {session_id}")
return session_id
except Exception as e:
logger.error(f"Ошибка при создании сессии для {username}: {type(e).__name__} - {str(e)}")
raise
def get_latest_session(self, client_id: str) -> Optional[Dict]:
"""Получает последнюю активную сессию для client_id"""
try:
self.clean_expired_sessions()
data = self._read_data()
latest_session = None
latest_time = None
for session_id, session in data["sessions"].items():
if session["client_id"] == client_id:
expiry = datetime.fromisoformat(session["expiry"])
if datetime.utcnow() > expiry:
logger.info(f"Сессия {session_id} истекла")
del data["sessions"][session_id]
continue
created_at = datetime.fromisoformat(session["created_at"])
if latest_time is None or created_at > latest_time:
latest_time = created_at
latest_session = session
latest_session["session_id"] = session_id
if latest_session:
logger.info(f"Найдена сессия для client_id {client_id}")
return latest_session
logger.info(f"Сессия для client_id {client_id} не найдена")
self._write_data(data)
return None
except Exception as e:
logger.error(f"Ошибка при получении сессии для client_id {client_id}: {type(e).__name__} - {str(e)}")
raise
def delete_session(self, session_id: str):
"""Удаляет сессию по session_id"""
try:
self.clean_expired_sessions()
data = self._read_data()
if session_id in data["sessions"]:
del data["sessions"][session_id]
self._write_data(data)
logger.info(f"Сессия с session_id {session_id} удалена")
except Exception as e:
logger.error(f"Ошибка при удалении сессии {session_id}: {type(e).__name__} - {str(e)}")
raise
def clean_expired_sessions(self):
"""Удаляет все истекшие сессии из хранилища"""
try:
data = self._read_data()
expired_sessions = []
current_time = datetime.utcnow()
# Находим истекшие сессии
for session_id, session in list(data["sessions"].items()):
expiry = datetime.fromisoformat(session["expiry"])
if current_time > expiry:
expired_sessions.append(session_id)
# Удаляем истекшие сессии
for session_id in expired_sessions:
del data["sessions"][session_id]
logger.info(f"Удалена истекшая сессия с session_id {session_id}")
# Записываем обновлённые данные
if expired_sessions:
self._write_data(data)
logger.info(f"Удалено {len(expired_sessions)} истекших сессий")
else:
logger.debug("Истекшие сессии не найдены")
except Exception as e:
logger.error(f"Ошибка при очистке истекших сессий: {type(e).__name__} - {str(e)}")
raise