test ci/cd
This commit is contained in:
commit
1e8243ab9a
265 changed files with 216965 additions and 0 deletions
9
SRV/.gitignore
vendored
Normal file
9
SRV/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.venv
|
||||
.idea
|
||||
__pycache__
|
||||
.gigaide
|
||||
.vscode
|
||||
.wheels
|
||||
.vendor
|
||||
.python3-deb
|
||||
storage/devices/
|
||||
0
SRV/README.md
Normal file
0
SRV/README.md
Normal file
0
SRV/__init__.py
Normal file
0
SRV/__init__.py
Normal file
0
SRV/api/__init__.py
Normal file
0
SRV/api/__init__.py
Normal file
14
SRV/api/router.py
Normal file
14
SRV/api/router.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from fastapi import APIRouter
|
||||
from .v1 import ufw, websocket, ip, network, auth, subnet, settings
|
||||
|
||||
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"])
|
||||
|
||||
router.include_router(settings.router, prefix="/settings", tags=["Settings"])
|
||||
0
SRV/api/v1/__init__.py
Normal file
0
SRV/api/v1/__init__.py
Normal file
191
SRV/api/v1/auth.py
Normal file
191
SRV/api/v1/auth.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
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):
|
||||
"""Регистрирует нового пользователя.
|
||||
|
||||
Создаёт нового пользователя на основе предоставленных данных и возвращает
|
||||
информацию о зарегистрированном пользователе.
|
||||
|
||||
Args:
|
||||
user (UserCreate): Объект с данными нового пользователя (username, password, is_admin).
|
||||
|
||||
Returns:
|
||||
UserResponse: Объект с данными зарегистрированного пользователя (id, username, is_admin).
|
||||
|
||||
Raises:
|
||||
HTTPException: Если пользователь уже существует (status_code=400) или произошла
|
||||
внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
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):
|
||||
"""Авторизует пользователя и создаёт сессию.
|
||||
|
||||
Проверяет учетные данные пользователя и возвращает токен и session_id для
|
||||
последующей авторизации.
|
||||
|
||||
Args:
|
||||
user (UserLogin): Объект с данными для входа (username, password).
|
||||
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
|
||||
|
||||
Returns:
|
||||
Token: Объект с access_token и session_id для авторизации.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если учетные данные неверны (status_code=401) или произошла
|
||||
внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
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):
|
||||
"""Возвращает данные текущего пользователя и токен.
|
||||
|
||||
Проверяет активную сессию по client_id и возвращает информацию о пользователе
|
||||
и токен для дальнейшего использования.
|
||||
|
||||
Args:
|
||||
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами "user" (UserResponse), "access_token" (str) и
|
||||
"token_type" (str, всегда "bearer").
|
||||
|
||||
Raises:
|
||||
HTTPException: Если сессия не найдена или истекла (status_code=401) или
|
||||
произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
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):
|
||||
"""Выход из системы, удаление сессии.
|
||||
|
||||
Удаляет активную сессию пользователя по client_id.
|
||||
|
||||
Args:
|
||||
request (Request): Объект запроса для получения client_id (IP-адрес клиента).
|
||||
|
||||
Returns:
|
||||
dict: Пустой словарь или сообщение об успешном выходе.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если сессия не найдена (status_code=401) или произошла
|
||||
внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
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 и содержит ли он пользователей.
|
||||
|
||||
Возвращает информацию о состоянии файла и наличии пользователей.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами:
|
||||
- "exists" (bool): Существует ли файл.
|
||||
- "has_users" (bool): Содержит ли файл пользователей.
|
||||
- "message" (str): Сообщение с описанием результата.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если произошла ошибка при проверке файла (status_code=500).
|
||||
"""
|
||||
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="Ошибка при проверке файла")
|
||||
518
SRV/api/v1/firewalld.py
Normal file
518
SRV/api/v1/firewalld.py
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
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)}",
|
||||
)
|
||||
98
SRV/api/v1/ip.py
Normal file
98
SRV/api/v1/ip.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
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-адреса перед его применением.
|
||||
|
||||
Выполняет ARP-сканирование для проверки, занят ли указанный IP-адрес,
|
||||
и возвращает результат проверки.
|
||||
|
||||
Args:
|
||||
data (Dict[str, Any]): Словарь с данными конфигурации, включая
|
||||
"ipv4_updates" с полем "ipv4.address-data" для нового IP-адреса.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Словарь с ключами:
|
||||
- "available" (bool): Доступен ли IP-адрес.
|
||||
- "message" (str): Сообщение о результате проверки.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
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-настройки к указанному сетевому профилю.
|
||||
|
||||
Обновляет настройки соединения через NetworkManager, отключает и
|
||||
переподключает устройство к сети.
|
||||
|
||||
Args:
|
||||
data (Dict[str, Any]): Словарь с данными конфигурации, включая
|
||||
"connection_id" и "ipv4_updates" с обновлениями IP-настроек.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами:
|
||||
- "success" (bool): Успешность операции.
|
||||
- "message" (str): Сообщение об успешном применении настроек.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если подключение не найдено (status_code=404) или
|
||||
произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
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-настройки для отката сетевого профиля.
|
||||
|
||||
Используется для сохранения и отката настроек сетевого профиля к предыдущему
|
||||
состоянию через NetworkManager.
|
||||
|
||||
Args:
|
||||
data (Dict[str, Any]): Словарь с данными конфигурации, включая
|
||||
"connection_id" и "ipv4_updates" с обновлениями IP-настроек.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами:
|
||||
- "success" (bool): Успешность операции.
|
||||
- "message" (str): Сообщение об успешном применении настроек.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если подключение не найдено (status_code=404) или
|
||||
произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
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)}")
|
||||
222
SRV/api/v1/network.py
Normal file
222
SRV/api/v1/network.py
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
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:
|
||||
"""Класс для управления позициями и состоянием контейнеров.
|
||||
|
||||
Хранит и обновляет координаты (x, y) и состояние (isOpen) для контейнеров
|
||||
активации и деактивации.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Инициализирует менеджер позиций контейнеров.
|
||||
|
||||
Создаёт начальные позиции и состояния для контейнеров "activation" и
|
||||
"deactivation" с координатами (0, 0) и закрытым состоянием (False).
|
||||
"""
|
||||
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]:
|
||||
"""Получает текущие координаты и состояние указанного контейнера.
|
||||
|
||||
Args:
|
||||
container_name (str): Имя контейнера ("activation" или "deactivation").
|
||||
|
||||
Returns:
|
||||
Tuple[int, int, bool]: Кортеж с координатами (x, y) и состоянием (isOpen).
|
||||
|
||||
Raises:
|
||||
ValueError: Если указан неверный идентификатор контейнера.
|
||||
"""
|
||||
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:
|
||||
"""Обновляет позицию и состояние контейнера по его идентификатору.
|
||||
|
||||
Args:
|
||||
container_id (int): Идентификатор контейнера (1 для "activation", 2 для "deactivation").
|
||||
x (int): Новая координата X.
|
||||
y (int): Новая координата Y.
|
||||
isOpen (bool): Новое состояние контейнера (открыт/закрыт).
|
||||
|
||||
Raises:
|
||||
ValueError: Если container_id не соответствует ни одному контейнеру.
|
||||
"""
|
||||
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]:
|
||||
"""Возвращает текущие сетевые данные в виде словаря.
|
||||
|
||||
Получает данные о сетевых интерфейсах, разделяет их на активные и неактивные,
|
||||
и возвращает их в формате Activation и Deactivation с учётом позиций и состояний
|
||||
контейнеров.
|
||||
|
||||
Returns:
|
||||
Dict[str, Dict]: Словарь с ключами "activation" и "deactivation", содержащими
|
||||
данные о соответствующих контейнерах.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если произошла ошибка при получении данных (status_code=500).
|
||||
"""
|
||||
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]]]:
|
||||
"""Разделяет интерфейсы на активные и неактивные.
|
||||
|
||||
Args:
|
||||
interfaces (List[Union[NetworkInterface, WiFiDevice]]): Список сетевых интерфейсов.
|
||||
|
||||
Returns:
|
||||
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(...)):
|
||||
"""Обновляет позицию и состояние контейнера.
|
||||
|
||||
Принимает данные о новом положении и состоянии контейнера и обновляет их
|
||||
в менеджере позиций.
|
||||
|
||||
Args:
|
||||
data (Dict[str, Union[int, bool]]): Словарь с ключами:
|
||||
- "container_id" (int): Идентификатор контейнера (1 или 2).
|
||||
- "container_x" (int): Новая координата X.
|
||||
- "container_y" (int): Новая координата Y.
|
||||
- "isOpen" (bool): Новое состояние контейнера (открыт/закрыт).
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами:
|
||||
- "success" (bool): Успешность операции.
|
||||
- "message" (str): Сообщение об успешном обновлении.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если отсутствуют обязательные поля (status_code=400) или
|
||||
типы данных неверны.
|
||||
"""
|
||||
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():
|
||||
"""Возвращает имя хоста системы.
|
||||
|
||||
Выполняет запрос к сервису для получения имени хоста.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключом "hostname" и значением имени хоста.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если произошла ошибка при получении имени хоста (status_code=500).
|
||||
"""
|
||||
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="Не удалось получить имя хоста")
|
||||
62
SRV/api/v1/settings.py
Normal file
62
SRV/api/v1/settings.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from services.settings_service import SettingsService
|
||||
|
||||
router = APIRouter()
|
||||
settings_service = SettingsService()
|
||||
|
||||
@router.post("/clear_subnet")
|
||||
def clear_subnet():
|
||||
"""Очищает данные узлов подсети, сохранённые в devices.json.
|
||||
|
||||
Удаляет все записи об узлах подсети, оставляя файл пустым.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешной очистке.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если произошла ошибка очистки (status_code=500).
|
||||
"""
|
||||
try:
|
||||
success = settings_service.clear_subnet()
|
||||
if success:
|
||||
return {"message": "Данные узлов подсети успешно очищены"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка очистки: {str(e)}")
|
||||
|
||||
@router.post("/clear_wifi")
|
||||
def clear_wifi():
|
||||
"""Очищает данные точек доступа Wi-Fi, сохранённые в wifi.json.
|
||||
|
||||
Удаляет все записи о точках доступа, оставляя файл пустым.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешной очистке.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если произошла ошибка очистки (status_code=500).
|
||||
"""
|
||||
try:
|
||||
success = settings_service.clear_wifi()
|
||||
if success:
|
||||
return {"message": "Данные точек доступа Wi-Fi успешно очищены"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка очистки: {str(e)}")
|
||||
|
||||
@router.post("/clear_all_data")
|
||||
def clear_all_data():
|
||||
"""Очищает данные узлов подсети и точек доступа Wi-Fi в обоих файлах.
|
||||
|
||||
Удаляет все записи из devices.json и wifi.json.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешной очистке.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если произошла ошибка очистки (status_code=500).
|
||||
"""
|
||||
try:
|
||||
success = settings_service.clear_all_data()
|
||||
if success:
|
||||
return {"message": "Все сетевые данные успешно очищены"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка очистки: {str(e)}")
|
||||
304
SRV/api/v1/subnet.py
Normal file
304
SRV/api/v1/subnet.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
from fastapi import APIRouter, HTTPException
|
||||
from typing import List, Dict
|
||||
from models.subnet import SubnetScanResult
|
||||
from models.wifi import WifiScanResult
|
||||
from models.request import NoteRequest, WifiConnectRequest, WifiDisconnectRequest
|
||||
from services.subnet_service import SubnetService
|
||||
from services.wifi_service import WifiService
|
||||
from services.connect_service import WifiConnectionService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/devices", response_model=SubnetScanResult)
|
||||
def scan_subnet(subnet: str = "192.168.1.0/24", ports: List[int] = None):
|
||||
"""Сканирует локальную подсеть и возвращает данные об устройствах.
|
||||
|
||||
Выполняет сканирование указанной подсети для обнаружения активных устройств
|
||||
с возможностью проверки указанных портов.
|
||||
|
||||
Args:
|
||||
subnet (str, optional): Адрес подсети в формате CIDR (например, "192.168.1.0/24").
|
||||
По умолчанию "192.168.1.0/24".
|
||||
ports (List[int], optional): Список портов для сканирования. По умолчанию None.
|
||||
|
||||
Returns:
|
||||
SubnetScanResult: Объект с результатами сканирования (устройства и их данные).
|
||||
|
||||
Raises:
|
||||
HTTPException: Если произошла ошибка сканирования (status_code=500).
|
||||
"""
|
||||
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 точки доступа и возвращает данные о них.
|
||||
|
||||
Выполняет сканирование доступных Wi-Fi сетей с указанным таймаутом.
|
||||
|
||||
Args:
|
||||
timeout (int, optional): Таймаут сканирования в секундах. По умолчанию 30.
|
||||
|
||||
Returns:
|
||||
WifiScanResult: Объект с результатами сканирования (список Wi-Fi сетей).
|
||||
|
||||
Raises:
|
||||
HTTPException: Если произошла ошибка сканирования Wi-Fi (status_code=500).
|
||||
"""
|
||||
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)}")
|
||||
|
||||
@router.post("/devices/{mac}/note")
|
||||
def add_note(mac: str, request: NoteRequest):
|
||||
"""Добавляет заметку для устройства по MAC-адресу.
|
||||
|
||||
Привязывает заметку к устройству, идентифицируемому по MAC-адресу.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
request (NoteRequest): Объект с полем "note" для текста заметки.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном добавлении заметки.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если устройство не найдено (status_code=404), заметка уже
|
||||
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
subnet_service = SubnetService()
|
||||
success, error_message = subnet_service.add_note(mac, request.note)
|
||||
if not success:
|
||||
if "не найдено" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "уже существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка '{request.note}' добавлена для устройства {mac}"}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка добавления заметки: {str(e)}")
|
||||
|
||||
@router.put("/devices/{mac}/note")
|
||||
def update_note(mac: str, request: NoteRequest):
|
||||
"""Обновляет заметку для устройства по MAC-адресу.
|
||||
|
||||
Изменяет существующую заметку, привязанную к устройству.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
request (NoteRequest): Объект с полем "note" для нового текста заметки.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном обновлении заметки.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если устройство не найдено (status_code=404), заметка не
|
||||
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
subnet_service = SubnetService()
|
||||
success, error_message = subnet_service.update_note(mac, request.note)
|
||||
if not success:
|
||||
if "не найдено" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "не существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка '{request.note}' обновлена для устройства {mac}"}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка обновления заметки: {str(e)}")
|
||||
|
||||
@router.delete("/devices/{mac}/note")
|
||||
def delete_note(mac: str):
|
||||
"""Удаляет заметку для устройства по MAC-адресу.
|
||||
|
||||
Удаляет привязанную заметку для указанного устройства.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном удалении заметки.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если устройство не найдено (status_code=404), заметка не
|
||||
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
subnet_service = SubnetService()
|
||||
success, error_message = subnet_service.delete_note(mac)
|
||||
if not success:
|
||||
if "не найдено" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "не существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка удалена для устройства {mac}"}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка удаления заметки: {str(e)}")
|
||||
|
||||
@router.post("/wifi/{ssid}/note")
|
||||
def add_wifi_note(ssid: str, request: NoteRequest):
|
||||
"""Добавляет заметку для сети Wi-Fi по SSID.
|
||||
|
||||
Привязывает заметку к Wi-Fi сети, идентифицируемой по SSID.
|
||||
|
||||
Args:
|
||||
ssid (str): Идентификатор Wi-Fi сети.
|
||||
request (NoteRequest): Объект с полем "note" для текста заметки.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном добавлении заметки.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если сеть не найдена (status_code=404), заметка уже
|
||||
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
wifi_service = WifiService()
|
||||
success, error_message = wifi_service.add_note(ssid, request.note)
|
||||
if not success:
|
||||
if "не найдена" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "уже существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка '{request.note}' добавлена для сети с SSID {ssid}"}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка добавления заметки: {str(e)}")
|
||||
|
||||
@router.put("/wifi/{ssid}/note")
|
||||
def update_wifi_note(ssid: str, request: NoteRequest):
|
||||
"""Обновляет заметку для сети Wi-Fi по SSID.
|
||||
|
||||
Изменяет существующую заметку, привязанную к Wi-Fi сети.
|
||||
|
||||
Args:
|
||||
ssid (str): Идентификатор Wi-Fi сети.
|
||||
request (NoteRequest): Объект с полем "note" для нового текста заметки.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном обновлении заметки.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если сеть не найдена (status_code=404), заметка не
|
||||
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
wifi_service = WifiService()
|
||||
success, error_message = wifi_service.update_note(ssid, request.note)
|
||||
if not success:
|
||||
if "не найдена" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "не существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка '{request.note}' обновлена для сети с SSID {ssid}"}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка обновления заметки: {str(e)}")
|
||||
|
||||
@router.delete("/wifi/{ssid}/note")
|
||||
def delete_wifi_note(ssid: str):
|
||||
"""Удаляет заметку для сети Wi-Fi по SSID.
|
||||
|
||||
Удаляет привязанную заметку для указанной Wi-Fi сети.
|
||||
|
||||
Args:
|
||||
ssid (str): Идентификатор Wi-Fi сети.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном удалении заметки.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если сеть не найдена (status_code=404), заметка не
|
||||
существует (status_code=400), или произошла внутренняя ошибка (status_code=500).
|
||||
"""
|
||||
try:
|
||||
wifi_service = WifiService()
|
||||
success, error_message = wifi_service.delete_note(ssid)
|
||||
if not success:
|
||||
if "не найдена" in error_message:
|
||||
raise HTTPException(status_code=404, detail=error_message)
|
||||
elif "не существует" in error_message:
|
||||
raise HTTPException(status_code=400, detail=error_message)
|
||||
return {"message": f"Заметка удалена для сети с SSID {ssid}"}
|
||||
except HTTPException as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка удаления заметки: {str(e)}")
|
||||
|
||||
@router.post("/wifi/connect")
|
||||
def connect_to_wifi(request: WifiConnectRequest):
|
||||
"""Подключается к Wi-Fi сети по SSID и паролю.
|
||||
|
||||
Выполняет подключение к указанной Wi-Fi сети с использованием предоставленных
|
||||
учетных данных.
|
||||
|
||||
Args:
|
||||
request (WifiConnectRequest): Объект с полями "ssid" и "password".
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном подключении.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если подключение не удалось (status_code=400).
|
||||
"""
|
||||
wifi_service = WifiConnectionService()
|
||||
success, message = wifi_service.connect(request.ssid, request.password)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return {"message": f"Успешно подключено к {request.ssid}"}
|
||||
|
||||
@router.post("/wifi/disconnect")
|
||||
def disconnect_from_wifi(request: WifiDisconnectRequest):
|
||||
"""Отключается от Wi-Fi сети по указанному SSID.
|
||||
|
||||
Выполняет отключение от указанной Wi-Fi сети.
|
||||
|
||||
Args:
|
||||
request (WifiDisconnectRequest): Объект с полем "ssid".
|
||||
|
||||
Returns:
|
||||
dict: Словарь с сообщением об успешном отключении.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если отключение не удалось (status_code=400).
|
||||
"""
|
||||
wifi_service = WifiConnectionService()
|
||||
success, message = wifi_service.disconnect(request.ssid)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return {"message": message or f"Успешно отключено от {request.ssid}"}
|
||||
|
||||
@router.get("/wifi/status")
|
||||
def get_wifi_status():
|
||||
"""Проверяет статус текущего подключения к Wi-Fi.
|
||||
|
||||
Возвращает информацию о текущем состоянии подключения и SSID, если подключение активно.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с ключами:
|
||||
- "status" (str): Состояние ("connected" или другое).
|
||||
- "ssid" (str, optional): SSID текущей сети, если подключено.
|
||||
|
||||
Raises:
|
||||
HTTPException: Если проверка статуса не удалась (status_code=400).
|
||||
"""
|
||||
wifi_service = WifiConnectionService()
|
||||
success, message = wifi_service.get_status()
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return {"status": "connected", "ssid": message}
|
||||
156
SRV/api/v1/ufw.py
Normal file
156
SRV/api/v1/ufw.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
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)}")
|
||||
152
SRV/api/v1/websocket.py
Normal file
152
SRV/api/v1/websocket.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
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-серверу.
|
||||
|
||||
Обрабатывает подключение, отключение и трансляцию сообщений для клиентов,
|
||||
подключённых через WebSocket.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Инициализирует менеджер подключений к WebSocket-серверу.
|
||||
|
||||
Создаёт пустое множество для хранения активных соединений.
|
||||
"""
|
||||
self.active_connections: Set[WebSocket] = set()
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
"""Подключает клиента к WebSocket-серверу.
|
||||
|
||||
Принимает подключение и добавляет WebSocket в список активных соединений.
|
||||
|
||||
Args:
|
||||
websocket (WebSocket): Объект WebSocket для подключения.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при принятии подключения.
|
||||
"""
|
||||
await websocket.accept()
|
||||
self.active_connections.add(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
"""Отключает клиента от WebSocket-серверу.
|
||||
|
||||
Удаляет WebSocket из списка активных соединений.
|
||||
|
||||
Args:
|
||||
websocket (WebSocket): Объект WebSocket для отключения.
|
||||
"""
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def broadcast(self, message: dict):
|
||||
"""Транслирует сообщения всем подключённым клиентам.
|
||||
|
||||
Отправляет JSON-данные всем активным соединениям, обрабатывая
|
||||
отключения клиентов.
|
||||
|
||||
Args:
|
||||
message (dict): Сообщение для трансляции в формате JSON.
|
||||
|
||||
Raises:
|
||||
WebSocketDisconnect: Если клиент неожиданно отключился.
|
||||
Exception: Если произошла другая ошибка при отправке.
|
||||
"""
|
||||
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-соединения.
|
||||
|
||||
Управляет соединением клиента, принимает данные для обновления позиций
|
||||
контейнеров и транслирует сетевые данные.
|
||||
|
||||
Args:
|
||||
websocket (WebSocket): Объект WebSocket для обработки соединения.
|
||||
|
||||
Raises:
|
||||
WebSocketDisconnect: Если клиент отключился.
|
||||
Exception: Если произошла ошибка при обработке данных.
|
||||
"""
|
||||
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():
|
||||
"""Запуск таска для трансляции сетевых данных при старте приложения.
|
||||
|
||||
Создаёт асинхронную задачу для периодической трансляции данных.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при запуске таска.
|
||||
"""
|
||||
asyncio.create_task(broadcast_data())
|
||||
|
||||
|
||||
async def broadcast_data():
|
||||
"""Таск для периодической трансляции сетевых данных.
|
||||
|
||||
Выполняет сканирование сети и отправляет данные всем подключённым клиентам
|
||||
с интервалом в 2 секунды.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при получении данных или трансляции.
|
||||
"""
|
||||
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)
|
||||
30
SRV/config/cors.py
Normal file
30
SRV/config/cors.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
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=["*"],
|
||||
)
|
||||
39
SRV/config/init_config.py
Normal file
39
SRV/config/init_config.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# 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()
|
||||
102
SRV/config/ports.py
Normal file
102
SRV/config/ports.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
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
0
SRV/core/__init__.py
Normal file
27
SRV/core/cli_executor.py
Normal file
27
SRV/core/cli_executor.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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")
|
||||
14
SRV/gonets.service
Executable file
14
SRV/gonets.service
Executable 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
22
SRV/main.py
Normal 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
0
SRV/models/__init__.py
Normal file
62
SRV/models/auth.py
Normal file
62
SRV/models/auth.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
"""
|
||||
Модель созданного пользователя
|
||||
|
||||
Атрибуты:
|
||||
username (str): Имя пользователя
|
||||
password (str): Пароль пользователя
|
||||
is_admin (bool): Административный доступ
|
||||
"""
|
||||
|
||||
username: str
|
||||
password: str
|
||||
is_admin: bool = False
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
"""
|
||||
Модель авторизации пользователя
|
||||
|
||||
Атрибуты:
|
||||
username (str): Имя пользователя
|
||||
password (str): Пароль пользователя
|
||||
"""
|
||||
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""
|
||||
Модель проверки наличия пользователя
|
||||
|
||||
Атрибуты:
|
||||
id (int): Идентификатор
|
||||
username (str): Имя пользователя
|
||||
is_admin (bool): Административный доступ
|
||||
"""
|
||||
|
||||
id: int
|
||||
username: str
|
||||
is_admin: bool
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
"""
|
||||
Токен пользователя
|
||||
|
||||
Атрибуты:
|
||||
access_token (str): Токен пользователя (захэширован)
|
||||
token_type (str): Тип токена
|
||||
session_id (Optional[str]): Идентификатор сессии
|
||||
user (Optional[UserResponse]): Пользователь на основе проверки наличия пользователя
|
||||
"""
|
||||
|
||||
access_token: str
|
||||
token_type: str
|
||||
session_id: Optional[str] = None
|
||||
user: Optional[UserResponse] = None
|
||||
109
SRV/models/firewalld.py
Normal file
109
SRV/models/firewalld.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
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")
|
||||
169
SRV/models/network.py
Normal file
169
SRV/models/network.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
from typing import List, Optional, Union
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class IPv4Route(BaseModel):
|
||||
"""
|
||||
Модель маршрута IPv4
|
||||
|
||||
Атрибуты:
|
||||
destination (str): IP-адрес назначения
|
||||
prefix (int): Префикс подсети в формате CIDR
|
||||
next_hop (str): Адрес следующего узла (маршрутизатор)
|
||||
metric (int): Метрика маршрута
|
||||
"""
|
||||
|
||||
destination: str
|
||||
prefix: int
|
||||
next_hop: str
|
||||
metric: int
|
||||
|
||||
|
||||
class IPv6Route(BaseModel):
|
||||
"""
|
||||
Модель маршрута IPv6
|
||||
|
||||
Атрибуты:
|
||||
destination (str): IP-адрес назначения
|
||||
prefix (int): Префикс подсети (CIDR)
|
||||
next_hop (str): Адрес следующего узла (маршрутизатор)
|
||||
metric (int): Метрика маршрута
|
||||
"""
|
||||
|
||||
destination: str
|
||||
prefix: int
|
||||
next_hop: str
|
||||
metric: int
|
||||
|
||||
|
||||
class IndividualIPConfig(BaseModel):
|
||||
"""
|
||||
Модель индивидуальной конфигурации IP-адреса
|
||||
|
||||
Атрибуты:
|
||||
address (str): IP-адрес интерфейса
|
||||
routes (List[Union[IPv4Route, IPv6Route]]): Список маршрутов (IPv4/IPv6)
|
||||
method (Optional[str]): Метод назначения IP
|
||||
gateway (str): Основной шлюз по умолчанию
|
||||
dns (List[str]): Список DNS-серверов
|
||||
"""
|
||||
|
||||
address: str
|
||||
routes: List[Union[IPv4Route, IPv6Route]]
|
||||
method: Optional[str]
|
||||
gateway: str
|
||||
dns: List[str]
|
||||
|
||||
|
||||
class IPConfig(BaseModel):
|
||||
"""
|
||||
Модель конфигурации IP (IPv4/IPv6)
|
||||
|
||||
Атрибуты:
|
||||
configurations (List[IndividualIPConfig]): Список индивидуальных IP-конфигураций
|
||||
"""
|
||||
|
||||
configurations: List[IndividualIPConfig]
|
||||
|
||||
|
||||
class NetworkInterface(BaseModel):
|
||||
"""
|
||||
Модель базового сетевого интерфейса
|
||||
|
||||
Атрибуты:
|
||||
name (str): Название интерфейса
|
||||
type (str): Тип интерфейса
|
||||
mac_address (str): MAC-адрес устройства
|
||||
device_state (str): Состояние устройства
|
||||
active_connection (str): Название активного соединения
|
||||
device_mtu (str): MTU устройства
|
||||
ipv4 (IPConfig): Конфигурация IPv4
|
||||
ipv6 (IPConfig): Конфигурация IPv6
|
||||
container_x (int): Положение по X на холсте
|
||||
container_y (int): Положение по Y на холсте
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
class WiFiDevice(NetworkInterface):
|
||||
"""
|
||||
Модель Wi-Fi устройства (наследуется от NetworkInterface)
|
||||
|
||||
Атрибуты:
|
||||
ssid (Optional[str]): Название Wi-Fi сети
|
||||
bssid (Optional[str]): MAC-адрес точки доступа
|
||||
bitrate (Optional[int]): Скорость соединения
|
||||
strength (Optional[int]): Уровень сигнала
|
||||
frequency (Optional[float]): Частота
|
||||
mode (Union[str, int]): Режим работы
|
||||
"""
|
||||
|
||||
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): Положение по оси X
|
||||
container_y (int): Положение по оси Y
|
||||
"""
|
||||
|
||||
interfaces: List[Union[NetworkInterface, WiFiDevice]]
|
||||
container_x: int
|
||||
container_y: int
|
||||
|
||||
|
||||
class Activation(BaseModel):
|
||||
"""
|
||||
Модель активированных интерфейсов
|
||||
|
||||
Атрибуты:
|
||||
id (int): Идентификатор ноды активированных интерфейсов
|
||||
interfaces (List[Union[NetworkInterface, WiFiDevice]]): Активированные интерфейсы
|
||||
container_x (int): Положение по X на холсте
|
||||
container_y (int): Положение по Y на холсте
|
||||
isOpen (bool): Флаг скрытия ноды на холсте
|
||||
"""
|
||||
|
||||
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): Положение по X на холсте
|
||||
container_y (int): Положение по Y на холсте
|
||||
isOpen (bool): Флаг скрытия ноды на холсте
|
||||
"""
|
||||
|
||||
id: int
|
||||
interfaces: List[Union[NetworkInterface, WiFiDevice]]
|
||||
container_x: int
|
||||
container_y: int
|
||||
isOpen: bool = True
|
||||
32
SRV/models/request.py
Normal file
32
SRV/models/request.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class NoteRequest(BaseModel):
|
||||
"""
|
||||
Модель добавления заметки к просканированному устройству
|
||||
|
||||
Атрибуты:
|
||||
note (str): Заметка устройства
|
||||
"""
|
||||
|
||||
note: str
|
||||
|
||||
class WifiConnectRequest(BaseModel):
|
||||
"""
|
||||
Модель для подключения к точке доступа Wi-Fi
|
||||
|
||||
Атрибуты:
|
||||
ssid (str): Название точки доступа Wi-Fi
|
||||
password (str): Пароль точки доступа Wi-Fi
|
||||
"""
|
||||
ssid: str
|
||||
password: str
|
||||
|
||||
class WifiDisconnectRequest(BaseModel):
|
||||
"""
|
||||
Модель для отключения от точки доступа Wi-Fi
|
||||
|
||||
Атрибуты:
|
||||
ssid (str): Название точки доступа Wi-Fi
|
||||
"""
|
||||
ssid: str
|
||||
38
SRV/models/subnet.py
Normal file
38
SRV/models/subnet.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class Device(BaseModel):
|
||||
"""
|
||||
Сканируемое устройство в подсети
|
||||
|
||||
Атрибуты:
|
||||
ip (str): IP-адрес устройства
|
||||
mac (Optional[str]): MAC-адрес устройства
|
||||
hostname (Optional[str]): Имя хоста
|
||||
vendor (Optional[str]): Название производителя устройства (По MAC-адресу)
|
||||
open_ports (List[str]): Открытые порты устройства
|
||||
status (str): Статус устройства (онлайн/оффлайн)
|
||||
last_seen (Optional[str]): Последняя активность устройства
|
||||
note (Optional[str]): Заметка устройства
|
||||
"""
|
||||
|
||||
ip: str
|
||||
mac: Optional[str] = None
|
||||
hostname: Optional[str] = None
|
||||
vendor: Optional[str] = None
|
||||
open_ports: List[str] = []
|
||||
status: str
|
||||
last_seen: Optional[str] = None
|
||||
note: Optional[str] = None
|
||||
|
||||
|
||||
class SubnetScanResult(BaseModel):
|
||||
"""
|
||||
Результат сканирования устройств в подсети
|
||||
|
||||
Атрибуты:
|
||||
devices (List[Device]): Список просканированных устройств
|
||||
"""
|
||||
|
||||
devices: List[Device] = []
|
||||
281
SRV/models/ufw.py
Normal file
281
SRV/models/ufw.py
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
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="Сообщение о статусе")
|
||||
40
SRV/models/wifi.py
Normal file
40
SRV/models/wifi.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class WifiAccessPoint(BaseModel):
|
||||
"""
|
||||
Модель точки доступа Wi-Fi
|
||||
|
||||
Атрибуты:
|
||||
ssid (Optional[str]): Название точки доступа. Может быть скрытым
|
||||
bssid (str): Уникальный идентификатор точки доступа (по типу MAC-адреса)
|
||||
channel (Optional[int]): Канал, на котором работает точка доступа
|
||||
signal (Optional[int]): Уровень сигнала (dBm)
|
||||
encryption (Optional[str]): Тип шифрования (WPA2, WPA3, открытая и т.д.)
|
||||
frequency (Optional[str]): Частота вещания (2.4GHz/5GHz)
|
||||
vendor (Optional[str]): Производитель точки доступа (определяется по MAC-адресу)
|
||||
hidden (bool): Флаг скрытия названия точки доступа (ssid)
|
||||
note (Optional[str]): Заметка для точки доступа
|
||||
"""
|
||||
|
||||
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
|
||||
note: Optional[str] = None
|
||||
|
||||
|
||||
class WifiScanResult(BaseModel):
|
||||
"""
|
||||
Результат сканирования Wi-Fi
|
||||
|
||||
Атрибуты:
|
||||
access_points (List[WifiAccessPoint]): Список найденных Wi-Fi точек доступа
|
||||
"""
|
||||
|
||||
access_points: List[WifiAccessPoint]
|
||||
2240
SRV/poetry.lock
generated
Normal file
2240
SRV/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
45
SRV/pyproject.toml
Normal file
45
SRV/pyproject.toml
Normal 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
0
SRV/services/__init__.py
Normal file
107
SRV/services/arp_service.py
Normal file
107
SRV/services/arp_service.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
from scapy.layers.l2 import ARP, Ether
|
||||
from scapy.sendrecv import srp
|
||||
import netifaces
|
||||
from netaddr import IPNetwork
|
||||
|
||||
|
||||
class NetworkScanner:
|
||||
"""Инициализация сетевого сканера.
|
||||
|
||||
Предоставляет методы для сканирования сети с использованием ARP-запросов и
|
||||
вывода информации о клиентах.
|
||||
"""
|
||||
|
||||
def __init__(self, interface=None):
|
||||
"""Инициализация параметров сетевого сканера.
|
||||
|
||||
Args:
|
||||
interface (str, optional): Название сетевого интерфейса. По умолчанию
|
||||
используется интерфейс по умолчанию из netifaces.gateways().
|
||||
|
||||
Инициализирует интерфейс, информацию об IP-адресах и локальный MAC-адрес.
|
||||
"""
|
||||
self.interface = (
|
||||
interface or netifaces.gateways()["default"][netifaces.AF_INET][1])
|
||||
self.ip_info = netifaces.ifaddresses(self.interface).get(
|
||||
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-пакет для отправки на указанный IP-адрес.
|
||||
|
||||
Args:
|
||||
target_ip (str): Целевой IP-адрес или подсеть для сканирования.
|
||||
|
||||
Returns:
|
||||
scapy.packet.Packet: Сформированный ARP-пакет с Ethernet-заголовком.
|
||||
"""
|
||||
arp = ARP(pdst=target_ip)
|
||||
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
|
||||
return ether / arp
|
||||
|
||||
def scan_network(self, attempts=3, timeout=2):
|
||||
"""Сканирует сеть для обнаружения активных клиентов.
|
||||
|
||||
Args:
|
||||
attempts (int, optional): Количество попыток отправки ARP-запросов.
|
||||
По умолчанию 3.
|
||||
timeout (int, optional): Таймаут ожидания ответа в секундах.
|
||||
По умолчанию 2.
|
||||
|
||||
Returns:
|
||||
List[dict]: Список словарей с ключами "ip" и "mac", представляющих
|
||||
обнаруженных клиентов, включая локальные IP-адреса.
|
||||
|
||||
Note:
|
||||
Учитывает локальные IP-адреса интерфейса и удаляет дубликаты.
|
||||
"""
|
||||
all_clients = []
|
||||
|
||||
for network in self.networks:
|
||||
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)
|
||||
|
||||
for ip_info in self.ip_info:
|
||||
local_client = {"ip": ip_info["addr"], "mac": self.local_mac}
|
||||
if local_client not in all_clients:
|
||||
all_clients.append(local_client)
|
||||
|
||||
unique_clients = {tuple(client.items()) for client in all_clients}
|
||||
return [dict(client) for client in unique_clients]
|
||||
|
||||
def print_clients(self, clients):
|
||||
"""Выводит список адресов клиентов в формате таблицы.
|
||||
|
||||
Args:
|
||||
clients (List[dict]): Список словарей с ключами "ip" и "mac",
|
||||
представляющих клиентов.
|
||||
|
||||
Выводит IP-адреса и MAC-адреса в отсортированном по IP порядке.
|
||||
"""
|
||||
print("Доступные устройства в сети:")
|
||||
print("IP" + " " * 18 + "MAC")
|
||||
for client in sorted(clients, key=lambda x: x["ip"]):
|
||||
print(f"{client['ip']:<16} {client['mac']}")
|
||||
|
||||
# Оставил нативный запуск сервиса для тестов
|
||||
if __name__ == "__main__":
|
||||
scanner = NetworkScanner()
|
||||
clients = scanner.scan_network()
|
||||
scanner.print_clients(clients)
|
||||
146
SRV/services/auth_service.py
Normal file
146
SRV/services/auth_service.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
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="Ошибка при выходе из системы")
|
||||
159
SRV/services/connect_service.py
Normal file
159
SRV/services/connect_service.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import subprocess
|
||||
import logging
|
||||
from typing import Tuple, Optional
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WifiConnectionService:
|
||||
"""Сервис для управления подключением к Wi-Fi сетям с использованием nmcli.
|
||||
|
||||
Предоставляет методы для подключения, отключения и проверки статуса Wi-Fi соединений.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Инициализирует сервис Wi-Fi.
|
||||
|
||||
Автоматически определяет доступный Wi-Fi-интерфейс и сохраняет его имя.
|
||||
"""
|
||||
self.interface = self._get_wifi_interface()
|
||||
|
||||
def _get_wifi_interface(self) -> str:
|
||||
"""Автоматически определяет Wi-Fi-интерфейс.
|
||||
|
||||
Returns:
|
||||
str: Название Wi-Fi-интерфейса (например, "wlan0") или пустая строка,
|
||||
если интерфейс не найден или Wi-Fi отключён.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при выполнении команд nmcli.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(["nmcli", "radio", "wifi"], capture_output=True, text=True)
|
||||
if "enabled" in result.stdout.lower():
|
||||
interfaces = subprocess.run(["nmcli", "device", "status"], capture_output=True, text=True).stdout
|
||||
for line in interfaces.splitlines():
|
||||
if "wifi" in line.lower() and "connected" in line.lower():
|
||||
iface = line.split()[0]
|
||||
logger.info(f"Используется Wi-Fi-интерфейс: {iface}")
|
||||
return iface
|
||||
logger.warning("Wi-Fi-интерфейс не найден или не подключён")
|
||||
return "wlan0"
|
||||
logger.error("Wi-Fi радио отключено")
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при определении Wi-Fi-интерфейса: {str(e)}")
|
||||
return ""
|
||||
|
||||
def connect(self, ssid: str, password: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Подключается к Wi-Fi сети по SSID и паролю.
|
||||
|
||||
Args:
|
||||
ssid (str): Идентификатор сети Wi-Fi.
|
||||
password (str): Пароль для подключения.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: Если команда nmcli завершилась с ошибкой.
|
||||
Exception: Если произошла неизвестная ошибка при выполнении.
|
||||
"""
|
||||
try:
|
||||
if not self.interface:
|
||||
return False, "Wi-Fi-интерфейс не доступен"
|
||||
|
||||
result = subprocess.run(["nmcli", "-f", "SSID", "device", "wifi", "list"], capture_output=True, text=True)
|
||||
if ssid not in result.stdout:
|
||||
return False, f"Сеть с SSID {ssid} не найдена в списке доступных сетей"
|
||||
|
||||
cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]
|
||||
process = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||
if process.returncode != 0:
|
||||
error_msg = process.stderr.lower()
|
||||
if "secrets were required" in error_msg or "authentication failed" in error_msg or "802-11-wireless-security.psk: недействительное свойство" in process.stderr:
|
||||
return False, f"Ошибка подключения к {ssid}: Неверный пароль"
|
||||
elif "no network with ssid" in error_msg:
|
||||
return False, f"Ошибка подключения к {ssid}: Сеть с SSID {ssid} недоступна или неверно указана"
|
||||
else:
|
||||
return False, f"Ошибка подключения к {ssid}: {process.stderr}"
|
||||
logger.info(f"Успешное подключение к сети {ssid}")
|
||||
return True, None
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка подключения к {ssid}: {e.stderr}")
|
||||
error_msg = e.stderr.lower()
|
||||
if "secrets were required" in error_msg or "authentication failed" in error_msg or "802-11-wireless-security.psk: недействительное свойство" in e.stderr:
|
||||
return False, f"Ошибка подключения к {ssid}: Неверный пароль"
|
||||
elif "no network with ssid" in error_msg:
|
||||
return False, f"Ошибка подключения к {ssid}: Сеть с SSID {ssid} недоступна или неверно указана"
|
||||
return False, f"Ошибка подключения к {ssid}: {e.stderr}"
|
||||
except Exception as e:
|
||||
logger.error(f"Неизвестная ошибка при подключении к {ssid}: {str(e)}")
|
||||
return False, f"Ошибка подключения к {ssid}: Неизвестная ошибка - {str(e)}"
|
||||
|
||||
def disconnect(self, ssid: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Отключается от Wi-Fi сети по указанному SSID.
|
||||
|
||||
Args:
|
||||
ssid (str): Идентификатор сети Wi-Fi для отключения.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None или сообщение, если подключение не найдено).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: Если команда nmcli завершилась с ошибкой.
|
||||
Exception: Если произошла неизвестная ошибка при выполнении.
|
||||
"""
|
||||
try:
|
||||
if not self.interface:
|
||||
return False, "Wi-Fi-интерфейс не доступен"
|
||||
result = subprocess.run(["nmcli", "connection", "show", "--active"], capture_output=True, text=True)
|
||||
active_connections = result.stdout.strip().splitlines()
|
||||
if not active_connections:
|
||||
logger.info(f"Нет активных подключений для SSID {ssid}")
|
||||
return True, f"Нет активного подключения для SSID {ssid}"
|
||||
for conn in active_connections:
|
||||
conn_details = conn.split()
|
||||
if len(conn_details) > 1:
|
||||
conn_name = conn_details[0]
|
||||
conn_info = subprocess.run(["nmcli", "connection", "show", conn_name], capture_output=True, text=True).stdout
|
||||
if "802-11-wireless.ssid:" in conn_info and ssid in conn_info:
|
||||
subprocess.run(["nmcli", "connection", "down", conn_name], capture_output=True, text=True, check=True)
|
||||
logger.info(f"Отключено соединение с SSID {ssid}")
|
||||
return True, None
|
||||
logger.warning(f"Активное подключение с SSID {ssid} не найдено")
|
||||
return True, f"Активное подключение с SSID {ssid} не найдено"
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка при отключении от {ssid}: {e.stderr}")
|
||||
return False, f"Ошибка отключения от {ssid}: {e.stderr}"
|
||||
except Exception as e:
|
||||
logger.error(f"Неизвестная ошибка при отключении от {ssid}: {str(e)}")
|
||||
return False, f"Ошибка отключения от {ssid}: {str(e)}"
|
||||
|
||||
def get_status(self) -> Tuple[bool, Optional[str]]:
|
||||
"""Проверяет статус текущего подключения к Wi-Fi.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, SSID или сообщение об ошибке).
|
||||
- Успех: (True, SSID текущей сети).
|
||||
- Ошибка или отсутствие подключения: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при выполнении команды nmcli.
|
||||
"""
|
||||
try:
|
||||
if not self.interface:
|
||||
return False, "Wi-Fi-интерфейс не доступен"
|
||||
result = subprocess.run(["nmcli", "connection", "show", "--active"], capture_output=True, text=True)
|
||||
if result.stdout.strip():
|
||||
ssid = result.stdout.splitlines()[1].split()[0]
|
||||
logger.info(f"Текущая сеть: {ssid}")
|
||||
return True, ssid
|
||||
return False, "Не подключено к Wi-Fi"
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке статуса: {str(e)}")
|
||||
return False, f"Ошибка проверки статуса: {str(e)}"
|
||||
149
SRV/services/firewalld_service.py
Normal file
149
SRV/services/firewalld_service.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
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)}")
|
||||
126
SRV/services/ip_service.py
Normal file
126
SRV/services/ip_service.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
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-адреса и применения настроек сети.
|
||||
|
||||
Предоставляет асинхронные методы для проверки занятости IP-адреса с использованием
|
||||
ARP-сканирования и применения настроек через NetworkManager.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Инициализирует сервис IP.
|
||||
|
||||
Создаёт объект ARP-сканера и устанавливает системную шину D-Bus как
|
||||
по умолчанию.
|
||||
"""
|
||||
self.arp_scanner = NetworkScanner()
|
||||
self.bus = sdbus.sd_bus_open_system()
|
||||
sdbus.set_default_bus(self.bus)
|
||||
|
||||
def _reset_bus(self):
|
||||
"""Перезапускает соединение с системной шиной D-Bus.
|
||||
|
||||
Закрывает текущее соединение и создаёт новое, устанавливая его как
|
||||
по умолчанию.
|
||||
"""
|
||||
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-адреса перед его применением.
|
||||
|
||||
Выполняет ARP-сканирование сети для проверки, занят ли указанный IP-адрес.
|
||||
Перезапускает шину D-Bus перед выполнением.
|
||||
|
||||
Args:
|
||||
args (Dict[str, Any]): Словарь с данными, включая "ipv4_updates"
|
||||
с полем "ipv4.address-data" для нового IP-адреса.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Словарь с ключами:
|
||||
- "available" (bool): Доступен ли IP-адрес.
|
||||
- "message" (str): Сообщение о результате проверки.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сканировании ARP.
|
||||
"""
|
||||
self._reset_bus()
|
||||
clients = self.arp_scanner.scan_network()
|
||||
new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"]
|
||||
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 к указанному соединению.
|
||||
|
||||
Перезапускает шину D-Bus, обновляет настройки соединения через NetworkManager,
|
||||
отключает и переподключает устройство к сети.
|
||||
|
||||
Args:
|
||||
args (Dict[str, Any]): Словарь с данными, включая:
|
||||
- "connection_id" (str): Идентификатор соединения.
|
||||
- "ipv4_updates" (Dict): Обновления настроек IPv4.
|
||||
|
||||
Raises:
|
||||
ValueError: Если соединение с указанным ID не найдено.
|
||||
RuntimeError: Если устройство для интерфейса не найдено.
|
||||
|
||||
Note:
|
||||
Требуются права для изменения настроек через NetworkManager.
|
||||
"""
|
||||
self._reset_bus()
|
||||
nm = NetworkManager()
|
||||
settings = NetworkManagerSettings()
|
||||
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} не найдено")
|
||||
371
SRV/services/network_service.py
Normal file
371
SRV/services/network_service.py
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
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.
|
||||
|
||||
Предоставляет асинхронные методы для получения информации о сетевых интерфейсах,
|
||||
их конфигурациях IP, Wi-Fi и состояниях.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Инициализирует сервис NetworkManager.
|
||||
|
||||
Устанавливает системный D-Bus и создаёт объекты для взаимодействия с
|
||||
NetworkManager и его настройками.
|
||||
"""
|
||||
self.system_bus = sd_bus_open_system()
|
||||
self.nm = NetworkManager(self.system_bus)
|
||||
self.nm_settings = NetworkManagerSettings(self.system_bus)
|
||||
|
||||
async def get_ip_method(self, device: NetworkDeviceGeneric, ip_type: str) -> str:
|
||||
"""Получает метод IP (например, "auto", "manual") для указанного устройства.
|
||||
|
||||
Args:
|
||||
device (NetworkDeviceGeneric): Объект устройства NetworkManager.
|
||||
ip_type (str): Тип IP (например, "ipv4" или "ipv6").
|
||||
|
||||
Returns:
|
||||
str: Метод IP или "unknown" в случае ошибки.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при получении настроек соединения.
|
||||
"""
|
||||
try:
|
||||
config = await device.get_applied_connection()
|
||||
settings = config[0].get(ip_type, {})
|
||||
return str(settings.get("method", ("s", "unknown"))[1])
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
async def get_connection_name(self, device: NetworkDeviceGeneric) -> str:
|
||||
"""Извлекает имя соединения (GENERAL.CONNECTION) для устройства.
|
||||
|
||||
Args:
|
||||
device (NetworkDeviceGeneric): Объект устройства NetworkManager.
|
||||
|
||||
Returns:
|
||||
str: Имя соединения или "unknown" в случае ошибки.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при получении настроек соединения.
|
||||
"""
|
||||
try:
|
||||
applied_connection = await device.get_applied_connection()
|
||||
connection_settings = applied_connection[0]
|
||||
connection_info = connection_settings.get("connection", {})
|
||||
return str(connection_info.get("id", ("s", "unknown"))[1])
|
||||
except Exception as e:
|
||||
return "unknown"
|
||||
|
||||
async def get_ip_config(self, device: NetworkDeviceGeneric, ip_version: str) -> List[IndividualIPConfig]:
|
||||
"""Получает конфигурацию IP для устройства.
|
||||
|
||||
Args:
|
||||
device (NetworkDeviceGeneric): Объект устройства NetworkManager.
|
||||
ip_version (str): Версия IP ("4" или "6").
|
||||
|
||||
Returns:
|
||||
List[IndividualIPConfig]: Список конфигураций IP, включая адреса и маршруты.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при получении данных IP.
|
||||
"""
|
||||
configs = []
|
||||
device_ip = await getattr(device, f"ip{ip_version}_config")
|
||||
if device_ip != "/":
|
||||
ip_config = globals()[f"IPv{ip_version}Config"](device_ip, self.system_bus)
|
||||
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 устройстве.
|
||||
|
||||
Args:
|
||||
wifi_device (NetworkDeviceWireless): Объект Wi-Fi устройства NetworkManager.
|
||||
|
||||
Returns:
|
||||
Dict[str, Optional[Union[str, int]]]: Словарь с данными Wi-Fi (SSID, BSSID,
|
||||
bitrate, strength, frequency, mode), где значения могут быть None.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при получении данных точки доступа.
|
||||
"""
|
||||
active_ap = await wifi_device.active_access_point
|
||||
wifi_info = {
|
||||
"ssid": None,
|
||||
"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]]:
|
||||
"""Получает список сетевых интерфейсов.
|
||||
|
||||
Args:
|
||||
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
|
||||
координат (x, y) для интерфейсов. По умолчанию None (используются (100, 100)).
|
||||
|
||||
Returns:
|
||||
List[Union[NetworkInterface, WiFiDevice]]: Список объектов интерфейсов,
|
||||
включая Wi-Fi устройства.
|
||||
|
||||
Исключает интерфейсы типа LOOPBACK и неактивные состояния (state == "10").
|
||||
"""
|
||||
devices_paths = await self.nm.get_devices()
|
||||
network_interfaces = []
|
||||
initial_positions = initial_positions or {}
|
||||
|
||||
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.
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: Словарь с ключом "hostname" и значением имени хоста.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Если произошла ошибка при выполнении команды hostname
|
||||
или обработке вывода.
|
||||
"""
|
||||
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]:
|
||||
"""Получает список интерфейсов по указанному состоянию.
|
||||
|
||||
Args:
|
||||
state (str): Состояние интерфейса (например, "100" для активных).
|
||||
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
|
||||
координат (x, y) для интерфейсов. По умолчанию None.
|
||||
|
||||
Returns:
|
||||
List[NetworkInterface]: Список интерфейсов с указанным состоянием.
|
||||
"""
|
||||
all_interfaces = await self.get_network_interfaces(initial_positions)
|
||||
return [
|
||||
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:
|
||||
"""Получает объект, содержащий список интерфейсов и координаты контейнера.
|
||||
|
||||
Args:
|
||||
state (Optional[str], optional): Состояние интерфейсов (например, "100").
|
||||
По умолчанию None (все интерфейсы).
|
||||
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
|
||||
координат (x, y) для интерфейсов. По умолчанию None.
|
||||
container_x (int, optional): Координата X контейнера. По умолчанию 0.
|
||||
container_y (int, optional): Координата Y контейнера. По умолчанию 0.
|
||||
|
||||
Returns:
|
||||
NetworkInterfacesContainer: Объект с интерфейсами и координатами контейнера.
|
||||
"""
|
||||
if state:
|
||||
interfaces = await self.get_interfaces_by_state(state, initial_positions)
|
||||
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:
|
||||
"""Получает контейнер с сетевыми интерфейсами и координатами контейнера.
|
||||
|
||||
Args:
|
||||
initial_positions (Optional[Dict[str, Tuple[int, int]]], optional):
|
||||
Словарь начальных координат (x, y) для интерфейсов. По умолчанию None.
|
||||
|
||||
Returns:
|
||||
NetworkInterfacesContainer: Объект с интерфейсами и координатами (0, 0).
|
||||
"""
|
||||
interfaces = await self.get_network_interfaces(initial_positions)
|
||||
return NetworkInterfacesContainer(interfaces=interfaces,
|
||||
container_x=0,
|
||||
container_y=0)
|
||||
|
||||
async def get_activated_interfaces(
|
||||
self,
|
||||
initial_positions: Dict[str, Tuple[int, int]] = None
|
||||
) -> List[NetworkInterface]:
|
||||
"""Получает список активных сетевых интерфейсов.
|
||||
|
||||
Args:
|
||||
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
|
||||
координат (x, y) для интерфейсов. По умолчанию None.
|
||||
|
||||
Returns:
|
||||
List[NetworkInterface]: Список активных интерфейсов (state == "100").
|
||||
"""
|
||||
return await self.get_interfaces_by_state("100", initial_positions)
|
||||
|
||||
async def get_deactivated_interfaces(
|
||||
self,
|
||||
initial_positions: Dict[str, Tuple[int, int]] = None
|
||||
) -> List[NetworkInterface]:
|
||||
"""Получает список неактивных сетевых интерфейсов.
|
||||
|
||||
Args:
|
||||
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
|
||||
координат (x, y) для интерфейсов. По умолчанию None.
|
||||
|
||||
Returns:
|
||||
List[NetworkInterface]: Список интерфейсов с state != "100".
|
||||
"""
|
||||
all_interfaces = await self.get_network_interfaces(initial_positions)
|
||||
return [
|
||||
interface for interface in all_interfaces
|
||||
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:
|
||||
"""Получает объект, содержащий список активных интерфейсов и координаты контейнера.
|
||||
|
||||
Args:
|
||||
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
|
||||
координат (x, y) для интерфейсов. По умолчанию None.
|
||||
container_x (int, optional): Координата X контейнера. По умолчанию 0.
|
||||
container_y (int, optional): Координата Y контейнера. По умолчанию 0.
|
||||
|
||||
Returns:
|
||||
NetworkInterfacesContainer: Объект с активными интерфейсами и координатами.
|
||||
"""
|
||||
return await self.get_interfaces_container("100", initial_positions, container_x, container_y)
|
||||
|
||||
async def get_deactivated_interfaces_container(
|
||||
self,
|
||||
initial_positions: Dict[str, Tuple[int, int]] = None,
|
||||
container_x: int = 0,
|
||||
container_y: int = 0,
|
||||
) -> NetworkInterfacesContainer:
|
||||
"""Получает объект, содержащий список неактивных интерфейсов и координаты контейнера.
|
||||
|
||||
Args:
|
||||
initial_positions (Dict[str, Tuple[int, int]], optional): Словарь начальных
|
||||
координат (x, y) для интерфейсов. По умолчанию None.
|
||||
container_x (int, optional): Координата X контейнера. По умолчанию 0.
|
||||
container_y (int, optional): Координата Y контейнера. По умолчанию 0.
|
||||
|
||||
Returns:
|
||||
NetworkInterfacesContainer: Объект с неактивными интерфейсами и координатами.
|
||||
"""
|
||||
interfaces = await self.get_deactivated_interfaces(initial_positions)
|
||||
return NetworkInterfacesContainer(interfaces=interfaces, container_x=container_x, container_y=container_y)
|
||||
68
SRV/services/settings_service.py
Normal file
68
SRV/services/settings_service.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SettingsService:
|
||||
"""Сервис для управления настройками и очистки данных сети."""
|
||||
|
||||
def __init__(self):
|
||||
self.subnet_file = "storage/devices/devices.json"
|
||||
self.wifi_file = "storage/wifi/wifi.json"
|
||||
|
||||
def clear_subnet(self) -> bool:
|
||||
"""Очищает данные узлов подсети в файле devices.json.
|
||||
|
||||
Returns:
|
||||
bool: True, если очистка успешна.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при записи в файл.
|
||||
"""
|
||||
try:
|
||||
with open(self.subnet_file, "w") as f:
|
||||
json.dump([], f)
|
||||
logger.info("Данные узлов подсети успешно очищены")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка очистки узлов подсети: {str(e)}")
|
||||
raise
|
||||
|
||||
def clear_wifi(self) -> bool:
|
||||
"""Очищает данные точек доступа Wi-Fi в файле wifi.json.
|
||||
|
||||
Returns:
|
||||
bool: True, если очистка успешна.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при записи в файл.
|
||||
"""
|
||||
try:
|
||||
with open(self.wifi_file, "w") as f:
|
||||
json.dump([], f)
|
||||
logger.info("Данные точек доступа Wi-Fi успешно очищены")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка очистки точек доступа Wi-Fi: {str(e)}")
|
||||
raise
|
||||
|
||||
def clear_all_data(self) -> bool:
|
||||
"""Очищает данные узлов подсети и точек доступа Wi-Fi в обоих файлах.
|
||||
|
||||
Returns:
|
||||
bool: True, если очистка успешна.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при записи в один из файлов.
|
||||
"""
|
||||
try:
|
||||
with open(self.subnet_file, "w") as f:
|
||||
json.dump([], f)
|
||||
with open(self.wifi_file, "w") as f:
|
||||
json.dump([], f)
|
||||
logger.info("Все сетевые данные успешно очищены")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка очистки всех сетевых данных: {str(e)}")
|
||||
raise
|
||||
594
SRV/services/subnet_service.py
Normal file
594
SRV/services/subnet_service.py
Normal file
|
|
@ -0,0 +1,594 @@
|
|||
from scapy.config import conf
|
||||
from mac_vendor_lookup import MacLookup
|
||||
from typing import List, Optional, Tuple, Dict
|
||||
import socket
|
||||
import logging
|
||||
import netifaces
|
||||
import ipaddress
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import time
|
||||
from functools import lru_cache
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
import subprocess
|
||||
|
||||
from models.subnet import SubnetScanResult, Device
|
||||
from config.ports import port_config
|
||||
from scapy.layers.inet import IP, TCP
|
||||
from scapy.layers.l2 import ARP, Ether
|
||||
from scapy.sendrecv import srp, sr1
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class NetworkScanner:
|
||||
"""Класс для сканирования подсети с использованием ARP-запросов.
|
||||
|
||||
Выполняет обнаружение устройств в указанной подсети через ARP-пакеты.
|
||||
"""
|
||||
|
||||
def __init__(self, interface: str):
|
||||
"""Инициализирует сканер сети с указанным сетевым интерфейсом.
|
||||
|
||||
Args:
|
||||
interface (str): Название сетевого интерфейса (например, "eth0").
|
||||
"""
|
||||
self.interface = interface
|
||||
conf.iface = interface
|
||||
|
||||
def scan(self, subnet: str) -> List[Tuple[str, str]]:
|
||||
"""Сканирует подсеть с использованием ARP-запросов.
|
||||
|
||||
Args:
|
||||
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, str]]: Список кортежей (IP-адрес, MAC-адрес) обнаруженных
|
||||
устройств.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при отправке ARP-пакетов.
|
||||
"""
|
||||
try:
|
||||
arp = ARP(pdst=subnet)
|
||||
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
|
||||
packet = ether / arp
|
||||
result = srp(packet,
|
||||
timeout=2,
|
||||
verbose=False,
|
||||
iface=self.interface)[0]
|
||||
return [(received.psrc, received.hwsrc)
|
||||
for sent, received in result]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка ARP-сканирования подсети {subnet}: {str(e)}")
|
||||
return []
|
||||
|
||||
class PortScanner:
|
||||
"""Класс для сканирования открытых портов на устройствах.
|
||||
|
||||
Выполняет сканирование портов с использованием TCP SYN-запросов.
|
||||
"""
|
||||
|
||||
def __init__(self, interface: str):
|
||||
"""Инициализирует сканер портов с указанным сетевым интерфейсом.
|
||||
|
||||
Args:
|
||||
interface (str): Название сетевого интерфейса (например, "eth0").
|
||||
"""
|
||||
self.interface = interface
|
||||
|
||||
def scan(self, ip: str) -> List[str]:
|
||||
"""Сканирует открытые порты на указанном IP-адресе.
|
||||
|
||||
Args:
|
||||
ip (str): IP-адрес устройства для сканирования.
|
||||
|
||||
Returns:
|
||||
List[str]: Список имён открытых портов (например, "HTTP", "SSH").
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сканировании портов.
|
||||
"""
|
||||
open_ports = []
|
||||
|
||||
def check_port(port: int) -> Optional[str]:
|
||||
"""Проверяет доступность конкретного порта.
|
||||
|
||||
Args:
|
||||
port (int): Номер порта для проверки.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Имя порта, если открыт, иначе None.
|
||||
"""
|
||||
try:
|
||||
pkt = sr1(
|
||||
IP(dst=ip) / TCP(dport=port, flags="S"),
|
||||
timeout=2,
|
||||
verbose=False,
|
||||
)
|
||||
if pkt and pkt.haslayer(TCP) and pkt[TCP].flags == 0x12:
|
||||
return port_config.get_port_name(port)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"Ошибка проверки порта {port} для IP {ip}: {str(e)}")
|
||||
return None
|
||||
|
||||
try:
|
||||
max_workers = min(len(port_config.get_ports()), 15)
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
results = executor.map(check_port, port_config.get_ports())
|
||||
open_ports = [port for port in results if port is not None]
|
||||
logger.debug(f"Открытые порты для IP {ip}: {open_ports}")
|
||||
return open_ports
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Ошибка при сканировании портов для IP {ip}: {str(e)}")
|
||||
return []
|
||||
|
||||
class DeviceInfoCollector:
|
||||
"""Класс для сбора информации об устройствах, включая вендоров и хостнеймы.
|
||||
|
||||
Управляет кэшированием данных NetBIOS и DNS.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Инициализирует коллектор информации об устройствах.
|
||||
|
||||
Создаёт объект MacLookup для определения вендоров и инициализирует кэши.
|
||||
"""
|
||||
self.mac_lookup = MacLookup()
|
||||
self.nbtscan_cache = {}
|
||||
self.nbtscan_results = {}
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
def get_vendor(self, mac: str) -> str:
|
||||
"""Определяет вендора по MAC-адресу с использованием кэширования.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства в формате XX:XX:XX:XX:XX:XX.
|
||||
|
||||
Returns:
|
||||
str: Название вендора или "Unknown", если вендор не определён.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при запросе к базе вендоров.
|
||||
"""
|
||||
try:
|
||||
return self.mac_lookup.lookup(mac)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Не удалось определить производителя для MAC {mac}: {str(e)}")
|
||||
return "Unknown"
|
||||
|
||||
def scan_nbtscan(self, subnet: str) -> None:
|
||||
"""Выполняет сканирование NetBIOS для указанной подсети.
|
||||
|
||||
Args:
|
||||
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
|
||||
|
||||
Сохраняет результаты в кэш, игнорируя хостнеймы "<unknown>".
|
||||
"""
|
||||
if subnet not in self.nbtscan_results:
|
||||
try:
|
||||
logger.debug(f"Запуск nbtscan для подсети {subnet}")
|
||||
result = subprocess.run(["sudo", "nbtscan", "-r", str(subnet)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=10)
|
||||
logger.debug(f"Полный вывод nbtscan: {result.stdout}")
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.splitlines():
|
||||
logger.debug(f"Обработка строки: {line}")
|
||||
if not line.startswith(("IP address", "-", "Doing NBT")):
|
||||
parts = [p for p in line.split() if p]
|
||||
if len(parts) >= 2:
|
||||
ip_addr, hostname = parts[0], parts[1]
|
||||
if hostname != "<unknown>":
|
||||
self.nbtscan_cache[ip_addr] = hostname
|
||||
logger.info(f"NetBIOS: Найден хостнейм {hostname} для IP {ip_addr}")
|
||||
self.nbtscan_results[subnet] = True
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error(f"NetBIOS: Таймаут для подсети {subnet}")
|
||||
except Exception as e:
|
||||
logger.error(f"NetBIOS: Ошибка для подсети {subnet}: {str(e)}")
|
||||
|
||||
def get_hostname(self, ip: str) -> Optional[str]:
|
||||
"""Определяет хостнейм для указанного IP-адреса.
|
||||
|
||||
Args:
|
||||
ip (str): IP-адрес устройства.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Хостнейм, если найден, иначе None.
|
||||
|
||||
Сначала пытается использовать DNS, затем NetBIOS через nbtscan.
|
||||
"""
|
||||
try:
|
||||
socket.setdefaulttimeout(2)
|
||||
return socket.gethostbyaddr(ip)[0]
|
||||
except (socket.herror, socket.gaierror, socket.timeout):
|
||||
logger.debug(f"DNS: Не удалось разрешить имя для IP {ip}")
|
||||
|
||||
subnet = ipaddress.ip_network(f"{ip}/24", strict=False)
|
||||
if subnet not in self.nbtscan_results:
|
||||
self.scan_nbtscan(str(subnet))
|
||||
|
||||
if ip in self.nbtscan_cache:
|
||||
return self.nbtscan_cache[ip]
|
||||
|
||||
logger.debug(f"Не удалось определить хостнейм для IP {ip} через DNS или NetBIOS")
|
||||
return None
|
||||
|
||||
class DeviceStorage:
|
||||
"""Класс для хранения и управления данными об устройствах.
|
||||
|
||||
Хранит устройства в JSON-файле и удаляет устаревшие записи.
|
||||
"""
|
||||
|
||||
def __init__(self, storage_path: str = "storage/devices/devices.json"):
|
||||
"""Инициализирует хранилище устройств с указанным путём.
|
||||
|
||||
Args:
|
||||
storage_path (str, optional): Путь к файлу хранения.
|
||||
По умолчанию "storage/devices/devices.json".
|
||||
|
||||
Создаёт родительские директории, если они не существуют.
|
||||
"""
|
||||
self.storage_file = Path(storage_path)
|
||||
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.offline_timeout = timedelta(days=7)
|
||||
|
||||
def load_devices(self) -> Dict[str, Device]:
|
||||
"""Загружает устройства из файла хранения.
|
||||
|
||||
Returns:
|
||||
Dict[str, Device]: Словарь устройств, где ключ — MAC-адрес.
|
||||
|
||||
Удаляет устройства с неверным форматом last_seen и возвращает пустой словарь
|
||||
при ошибке.
|
||||
"""
|
||||
try:
|
||||
if self.storage_file.exists():
|
||||
with open(self.storage_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
devices = {}
|
||||
for device_data in data.get("devices", []):
|
||||
device = Device(**device_data)
|
||||
if device.mac:
|
||||
devices[device.mac] = device
|
||||
return self._clean_old_devices(devices)
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при загрузке устройств: {str(e)}")
|
||||
return {}
|
||||
|
||||
def _clean_old_devices(self, devices: Dict[str, Device]) -> Dict[str, Device]:
|
||||
"""Очищает устаревшие устройства из словаря.
|
||||
|
||||
Args:
|
||||
devices (Dict[str, Device]): Словарь устройств с MAC-адресами как ключами.
|
||||
|
||||
Returns:
|
||||
Dict[str, Device]: Отфильтрованный словарь, исключающий устройства
|
||||
offline более 7 дней.
|
||||
"""
|
||||
now = datetime.now()
|
||||
filtered = {}
|
||||
for mac, device in devices.items():
|
||||
if device.status == "offline" and device.last_seen:
|
||||
try:
|
||||
last_seen = datetime.fromisoformat(device.last_seen)
|
||||
if now - last_seen <= self.offline_timeout:
|
||||
filtered[mac] = device
|
||||
else:
|
||||
logger.info(f"Удалено устройство {mac} (offline > 7 дней)")
|
||||
except ValueError:
|
||||
logger.warning(f"Неверный формат last_seen для устройства {mac}")
|
||||
else:
|
||||
filtered[mac] = device
|
||||
return filtered
|
||||
|
||||
def save_devices(self, devices: List[Device]) -> None:
|
||||
"""Сохраняет список устройств в файл.
|
||||
|
||||
Args:
|
||||
devices (List[Device]): Список объектов Device для сохранения.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при записи в файл.
|
||||
"""
|
||||
try:
|
||||
data = SubnetScanResult(devices=devices).model_dump()
|
||||
with open(self.storage_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
logger.info(f"Сохранено {len(devices)} устройств в {self.storage_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении устройств: {str(e)}")
|
||||
|
||||
class SubnetService:
|
||||
"""Сервис для сканирования подсетей и управления устройствами.
|
||||
|
||||
Объединяет функционал сканирования сети, портов и хранения данных об устройствах.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Инициализирует сервис подсети с настройкой всех компонентов.
|
||||
|
||||
Автоматически определяет интерфейс и инициализирует сканеры и хранилище.
|
||||
"""
|
||||
self.interface = self._get_default_interface()
|
||||
self.network_scanner = NetworkScanner(self.interface)
|
||||
self.port_scanner = PortScanner(self.interface)
|
||||
self.device_info = DeviceInfoCollector()
|
||||
self.storage = DeviceStorage()
|
||||
|
||||
def _get_default_interface(self) -> str:
|
||||
"""Определяет имя сетевого интерфейса по умолчанию.
|
||||
|
||||
Returns:
|
||||
str: Название интерфейса (например, "eth0").
|
||||
|
||||
Использует информацию о шлюзе или первый доступный интерфейс, отличный от "lo".
|
||||
"""
|
||||
try:
|
||||
gateways = netifaces.gateways()
|
||||
default_gateway = gateways.get("default", {}).get(netifaces.AF_INET)
|
||||
if default_gateway:
|
||||
return default_gateway[1]
|
||||
interfaces = netifaces.interfaces()
|
||||
for iface in interfaces:
|
||||
if iface != "lo":
|
||||
return iface
|
||||
return "eth0"
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось определить интерфейс: {str(e)}")
|
||||
return "eth0"
|
||||
|
||||
def scan_subnet(self, subnet: str, ports: List[int] = None) -> SubnetScanResult:
|
||||
"""Сканирует указанную подсеть и возвращает результаты.
|
||||
|
||||
Args:
|
||||
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
|
||||
ports (List[int], optional): Список портов для сканирования.
|
||||
По умолчанию None (используются порты из config.ports).
|
||||
|
||||
Returns:
|
||||
SubnetScanResult: Объект с результатами сканирования, содержащий список
|
||||
устройств.
|
||||
|
||||
Сохраняет данные в хранилище и обновляет статус устройств.
|
||||
"""
|
||||
start_time = time.time()
|
||||
devices = []
|
||||
now = datetime.now().astimezone()
|
||||
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
|
||||
try:
|
||||
saved_devices = self.storage.load_devices()
|
||||
|
||||
local_device = self._get_local_device(subnet)
|
||||
if local_device:
|
||||
local_device.status = "online"
|
||||
local_device.last_seen = formatted_now
|
||||
devices.append(local_device)
|
||||
if local_device.mac:
|
||||
saved_devices[local_device.mac] = local_device
|
||||
|
||||
discovered_devices = self.network_scanner.scan(subnet)
|
||||
self.device_info.scan_nbtscan(subnet)
|
||||
|
||||
def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]:
|
||||
"""Обрабатывает устройство из пары IP и MAC.
|
||||
|
||||
Args:
|
||||
ip_mac (Tuple[str, str]): Кортеж с IP-адресом и MAC-адресом.
|
||||
|
||||
Returns:
|
||||
Optional[Device]: Объект Device или None, если устройство локальное.
|
||||
"""
|
||||
ip, mac = ip_mac
|
||||
if local_device and ip == local_device.ip:
|
||||
return None
|
||||
|
||||
existing_device = saved_devices.get(mac)
|
||||
if existing_device:
|
||||
return Device(
|
||||
ip=ip,
|
||||
mac=mac,
|
||||
hostname=self.device_info.get_hostname(ip) or existing_device.hostname,
|
||||
vendor=self.device_info.get_vendor(mac),
|
||||
open_ports=self.port_scanner.scan(ip) or existing_device.open_ports,
|
||||
status="online",
|
||||
last_seen=formatted_now,
|
||||
note=existing_device.note,
|
||||
)
|
||||
vendor = self.device_info.get_vendor(mac)
|
||||
hostname = self.device_info.get_hostname(ip)
|
||||
open_ports = self.port_scanner.scan(ip)
|
||||
return Device(
|
||||
ip=ip,
|
||||
mac=mac,
|
||||
hostname=hostname,
|
||||
vendor=vendor,
|
||||
open_ports=open_ports,
|
||||
status="online",
|
||||
last_seen=formatted_now,
|
||||
note=None,
|
||||
)
|
||||
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
device_results = executor.map(process_device, discovered_devices)
|
||||
devices.extend([d for d in device_results if d is not None])
|
||||
|
||||
active_macs = {d.mac for d in devices if d.mac}
|
||||
for mac, device in saved_devices.items():
|
||||
if mac not in active_macs:
|
||||
device.status = "offline"
|
||||
devices.append(device)
|
||||
|
||||
devices.sort(key=lambda x: int(ipaddress.IPv4Address(x.ip)))
|
||||
|
||||
for device in devices:
|
||||
if device.mac:
|
||||
saved_devices[device.mac] = device
|
||||
self.storage.save_devices(list(saved_devices.values()))
|
||||
|
||||
logger.info(
|
||||
f"Сканирование подсети {subnet} завершено за {time.time() - start_time:.2f} секунд, "
|
||||
f"найдено {len(devices)} устройств (активных: {len(active_macs)})"
|
||||
)
|
||||
return SubnetScanResult(devices=devices)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сканировании подсети {subnet}: {str(e)}")
|
||||
return SubnetScanResult(devices=devices)
|
||||
|
||||
def _get_local_device(self, subnet: str) -> Optional[Device]:
|
||||
"""Определяет локальное устройство в указанной подсети.
|
||||
|
||||
Args:
|
||||
subnet (str): Подсеть в формате CIDR (например, "192.168.1.0/24").
|
||||
|
||||
Returns:
|
||||
Optional[Device]: Объект локального устройства или None, если не найдено.
|
||||
"""
|
||||
now = datetime.now().astimezone()
|
||||
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
|
||||
try:
|
||||
subnet_network = ipaddress.ip_network(subnet, strict=False)
|
||||
interfaces = netifaces.interfaces()
|
||||
for iface in interfaces:
|
||||
if iface == "lo":
|
||||
continue
|
||||
addrs = netifaces.ifaddresses(iface)
|
||||
if netifaces.AF_INET in addrs and netifaces.AF_LINK in addrs:
|
||||
ip = addrs[netifaces.AF_INET][0]["addr"]
|
||||
if ipaddress.ip_address(ip) in subnet_network:
|
||||
mac = addrs[netifaces.AF_LINK][0]["addr"]
|
||||
vendor = self.device_info.get_vendor(mac)
|
||||
hostname = self.device_info.get_hostname(ip)
|
||||
open_ports = self.port_scanner.scan(ip)
|
||||
logger.debug(
|
||||
f"Локальное устройство: IP={ip}, MAC={mac}, Ports={open_ports}, Hostname={hostname}"
|
||||
)
|
||||
return Device(
|
||||
ip=ip,
|
||||
mac=mac,
|
||||
hostname=hostname,
|
||||
vendor=vendor,
|
||||
open_ports=open_ports,
|
||||
status="online",
|
||||
last_seen=formatted_now,
|
||||
note=None,
|
||||
)
|
||||
logger.warning(
|
||||
f"Локальное устройство не найдено в подсети {subnet}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Не удалось получить локальное устройство: {str(e)}")
|
||||
return None
|
||||
|
||||
def add_note(self, mac: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Добавляет заметку к устройству по MAC-адресу.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
note (str): Текст заметки.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении.
|
||||
"""
|
||||
try:
|
||||
saved_devices = self.storage.load_devices()
|
||||
if mac not in saved_devices:
|
||||
logger.warning(f"Устройство с MAC {mac} не найдено")
|
||||
return False, f"Устройство с MAC {mac} не найдено"
|
||||
if saved_devices[mac].note is not None:
|
||||
logger.error(
|
||||
f"Заметка для устройства с MAC {mac} уже существует")
|
||||
return False, f"Заметка для устройства с MAC {mac} уже существует"
|
||||
saved_devices[mac].note = note
|
||||
self.storage.save_devices(list(saved_devices.values()))
|
||||
logger.info(
|
||||
f"Добавлена заметка '{note}' для устройства с MAC {mac}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка при добавлении заметки для MAC {mac}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
|
||||
def update_note(self, mac: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Обновляет заметку устройства по MAC-адресу.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
note (str): Новый текст заметки.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении.
|
||||
"""
|
||||
try:
|
||||
saved_devices = self.storage.load_devices()
|
||||
if mac not in saved_devices:
|
||||
logger.warning(f"Устройство с MAC {mac} не найдено")
|
||||
return False, f"Устройство с MAC {mac} не найдено"
|
||||
if saved_devices[mac].note is None:
|
||||
logger.error(
|
||||
f"Заметка для устройства с MAC {mac} не существует")
|
||||
return False, f"Заметка для устройства с MAC {mac} не существует"
|
||||
saved_devices[mac].note = note
|
||||
self.storage.save_devices(list(saved_devices.values()))
|
||||
logger.info(
|
||||
f"Обновлена заметка '{note}' для устройства с MAC {mac}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка при обновлении заметки для MAC {mac}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
|
||||
def delete_note(self, mac: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Удаляет заметку устройства по MAC-адресу.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении.
|
||||
"""
|
||||
try:
|
||||
saved_devices = self.storage.load_devices()
|
||||
if mac not in saved_devices:
|
||||
logger.warning(f"Устройство с MAC {mac} не найдено")
|
||||
return False, f"Устройство с MAC {mac} не найдено"
|
||||
if saved_devices[mac].note is None:
|
||||
logger.error(
|
||||
f"Заметка для устройства с MAC {mac} не существует")
|
||||
return False, f"Заметка для устройства с MAC {mac} не существует"
|
||||
saved_devices[mac].note = None
|
||||
self.storage.save_devices(list(saved_devices.values()))
|
||||
logger.info(f"Удалена заметка для устройства с MAC {mac}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка при удалении заметки для MAC {mac}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
402
SRV/services/ufw_service.py
Normal file
402
SRV/services/ufw_service.py
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
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": []}
|
||||
486
SRV/services/wifi_service.py
Normal file
486
SRV/services/wifi_service.py
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
import time
|
||||
import logging
|
||||
import subprocess
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
from mac_vendor_lookup import MacLookup
|
||||
import pywifi
|
||||
from models.wifi import WifiAccessPoint, WifiScanResult
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WifiStorage:
|
||||
"""Класс для хранения данных о Wi-Fi сетях и их заметках по SSID.
|
||||
|
||||
Предоставляет методы для загрузки и сохранения заметок, связанных с SSID сетей,
|
||||
в JSON-файле на диске.
|
||||
"""
|
||||
|
||||
def __init__(self, storage_path: str = "storage/wifi/wifi.json"):
|
||||
"""Инициализирует объект хранилища Wi-Fi с указанным путём к файлу.
|
||||
|
||||
Args:
|
||||
storage_path (str, optional): Путь к файлу для хранения заметок.
|
||||
По умолчанию "storage/wifi/wifi.json".
|
||||
|
||||
Создаёт родительские директории, если они не существуют.
|
||||
"""
|
||||
self.storage_file = Path(storage_path)
|
||||
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def load_notes(self) -> Dict[str, Optional[str]]:
|
||||
"""Загружает заметки по SSID из файла хранения.
|
||||
|
||||
Returns:
|
||||
Dict[str, Optional[str]]: Словарь, где ключ — SSID, значение — заметка
|
||||
или None, если заметки нет.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при чтении или парсинге файла.
|
||||
"""
|
||||
try:
|
||||
if self.storage_file.exists():
|
||||
with open(self.storage_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return {item["ssid"]: item.get("note") for item in data.get("networks", [])}
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при загрузке заметок: {str(e)}")
|
||||
return {}
|
||||
|
||||
def save_notes(self, notes: Dict[str, Optional[str]]) -> None:
|
||||
"""Сохраняет заметки по SSID в файл.
|
||||
|
||||
Args:
|
||||
notes (Dict[str, Optional[str]]): Словарь с SSID как ключами и заметками
|
||||
как значениями.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при записи в файл.
|
||||
"""
|
||||
try:
|
||||
data = {"networks": [{"ssid": ssid, "note": note} for ssid, note in notes.items()]}
|
||||
with open(self.storage_file, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
logger.info(f"Сохранено {len(notes)} заметок в {self.storage_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении заметок: {str(e)}")
|
||||
|
||||
class WifiService:
|
||||
"""Сервис для сканирования текущих Wi-Fi точек доступа.
|
||||
|
||||
Предоставляет функционал для обнаружения Wi-Fi сетей с использованием pywifi и iw,
|
||||
определения характеристик точек доступа (сигнал, шифрование, канал и т.д.),
|
||||
а также управления заметками.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Инициализирует сервис Wi-Fi с настройкой интерфейса и хранилища.
|
||||
|
||||
Инициализирует объект MacLookup для определения вендоров по MAC-адресам,
|
||||
PyWiFi для работы с Wi-Fi-интерфейсами и объект хранилища заметок.
|
||||
"""
|
||||
self.mac_lookup = MacLookup()
|
||||
self.wifi = pywifi.PyWiFi()
|
||||
self.interface = self._get_wifi_interface()
|
||||
self.storage = WifiStorage()
|
||||
|
||||
def _get_wifi_interface(self) -> Optional[object]:
|
||||
"""Автоматически определяет активный Wi-Fi-интерфейс.
|
||||
|
||||
Returns:
|
||||
Optional[object]: Объект интерфейса PyWiFi или None, если интерфейс не найден.
|
||||
|
||||
Проверяет доступные интерфейсы, выполняет тестовое сканирование и возвращает
|
||||
первый активный интерфейс, исключая P2P-интерфейсы.
|
||||
"""
|
||||
try:
|
||||
interfaces = self.wifi.interfaces()
|
||||
if not interfaces:
|
||||
logger.error("Не найдено сетевых интерфейсов")
|
||||
return None
|
||||
|
||||
logger.debug(
|
||||
f"Доступные интерфейсы: {[iface.name() for iface in interfaces]}"
|
||||
)
|
||||
|
||||
for iface in interfaces:
|
||||
iface_name = iface.name()
|
||||
if "p2p" in iface_name.lower():
|
||||
logger.debug(f"Пропущен интерфейс {iface_name} (P2P)")
|
||||
continue
|
||||
try:
|
||||
iface.scan()
|
||||
time.sleep(1)
|
||||
results = iface.scan_results()
|
||||
if results:
|
||||
logger.info(f"Выбран Wi-Fi-интерфейс: {iface_name}")
|
||||
return iface
|
||||
logger.debug(
|
||||
f"Интерфейс {iface_name} не вернул результатов сканирования"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"Интерфейс {iface_name} неактивен: {str(e)}")
|
||||
|
||||
logger.error("Не найден активный Wi-Fi-интерфейс")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при определении Wi-Fi-интерфейса: {str(e)}")
|
||||
return None
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
def get_vendor(self, mac: str) -> str:
|
||||
"""Определяет вендора по MAC-адресу с использованием кэширования.
|
||||
|
||||
Args:
|
||||
mac (str): MAC-адрес устройства в формате XX:XX:XX:XX:XX:XX.
|
||||
|
||||
Returns:
|
||||
str: Название вендора или "Unknown", если вендор не определён.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при запросе к базе вендоров.
|
||||
"""
|
||||
try:
|
||||
return self.mac_lookup.lookup(mac)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Не удалось определить производителя для MAC {mac}: {str(e)}")
|
||||
return "Unknown"
|
||||
|
||||
def _get_encryption_from_iw(self, ap_info: Dict) -> str:
|
||||
"""Определяет тип шифрования из данных, полученных через iw.
|
||||
|
||||
Args:
|
||||
ap_info (Dict): Словарь с данными точки доступа, включая поле "security".
|
||||
|
||||
Returns:
|
||||
str: Тип шифрования ("WPA3", "WPA2", "WPA", "WEP" или "Open").
|
||||
"""
|
||||
security = ap_info.get("security", "")
|
||||
logger.debug(
|
||||
f"Шифрование для BSSID {ap_info.get('bssid')}: {security}")
|
||||
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]:
|
||||
"""Преобразует частоту в номер канала.
|
||||
|
||||
Args:
|
||||
frequency (Optional[int]): Частота в МГц или None.
|
||||
|
||||
Returns:
|
||||
Optional[int]: Номер канала или None, если частота вне диапазона.
|
||||
"""
|
||||
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]:
|
||||
"""Определяет частотный диапазон на основе частоты.
|
||||
|
||||
Args:
|
||||
frequency (Optional[int]): Частота в МГц или None.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Диапазон ("2.4 GHz", "5 GHz" или None).
|
||||
"""
|
||||
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]:
|
||||
"""Получает данные сканирования Wi-Fi через команду iw.
|
||||
|
||||
Returns:
|
||||
List[Dict]: Список словарей с данными точек доступа (BSSID, SSID, частота,
|
||||
сигнал, шифрование).
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: Если команда iw завершилась с ошибкой.
|
||||
subprocess.TimeoutExpired: Если превышен таймаут выполнения.
|
||||
Exception: Если произошла ошибка при парсинге вывода.
|
||||
"""
|
||||
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 точки доступа.
|
||||
|
||||
Args:
|
||||
timeout (int, optional): Таймаут ожидания в секундах. По умолчанию 30.
|
||||
|
||||
Returns:
|
||||
WifiScanResult: Объект с результатами сканирования, содержащий список точек
|
||||
доступа.
|
||||
|
||||
Сканирует Wi-Fi с использованием pywifi и iw, объединяет данные, определяет
|
||||
лучшие точки по сигналу и сохраняет заметки.
|
||||
"""
|
||||
start_time = time.time()
|
||||
aps = []
|
||||
|
||||
if not self.interface:
|
||||
logger.error("Wi-Fi-интерфейс не доступен")
|
||||
return WifiScanResult(access_points=aps)
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"Начало сканирования Wi-Fi на интерфейсе {self.interface.name()}"
|
||||
)
|
||||
self.interface.scan()
|
||||
time.sleep(10)
|
||||
py_results = self.interface.scan_results()
|
||||
logger.debug(f"Найдено {len(py_results)} точек через pywifi")
|
||||
|
||||
iw_results = self._get_iw_scan_data()
|
||||
iw_by_bssid = {ap["bssid"]: ap for ap in iw_results if ap.get("bssid")}
|
||||
|
||||
best_aps_by_ssid: Dict[str, WifiAccessPoint] = {}
|
||||
saved_notes = self.storage.load_notes()
|
||||
|
||||
for ap in py_results:
|
||||
bssid = ap.bssid.lower() if ap.bssid else None
|
||||
if not bssid:
|
||||
logger.debug("Пропущена точка без BSSID")
|
||||
continue
|
||||
ssid = ap.ssid if ap.ssid else ""
|
||||
hidden = not ssid
|
||||
signal = ap.signal if ap.signal else None
|
||||
iw_data = iw_by_bssid.get(bssid, {})
|
||||
|
||||
frequency = iw_data.get("frequency")
|
||||
channel = self._get_channel(frequency)
|
||||
freq_band = self._get_frequency(frequency)
|
||||
encryption = self._get_encryption_from_iw(iw_data)
|
||||
vendor = self.get_vendor(bssid)
|
||||
|
||||
logger.debug(
|
||||
f"Точка {bssid}: freq={frequency}, channel={channel}, encryption={encryption}, signal={signal}"
|
||||
)
|
||||
|
||||
current_ap = WifiAccessPoint(
|
||||
ssid=ssid,
|
||||
bssid=bssid,
|
||||
channel=channel,
|
||||
signal=signal if signal else iw_data.get("signal"),
|
||||
encryption=encryption,
|
||||
frequency=freq_band,
|
||||
vendor=vendor,
|
||||
hidden=hidden,
|
||||
)
|
||||
|
||||
if ssid not in best_aps_by_ssid or (
|
||||
current_ap.signal and best_aps_by_ssid[ssid].signal
|
||||
and current_ap.signal > best_aps_by_ssid[ssid].signal):
|
||||
current_ap.note = saved_notes.get(ssid)
|
||||
best_aps_by_ssid[ssid] = current_ap
|
||||
|
||||
aps = list(best_aps_by_ssid.values())
|
||||
notes_to_save = {ap.ssid: ap.note for ap in aps if ap.ssid}
|
||||
self.storage.save_notes(notes_to_save)
|
||||
|
||||
logger.info(
|
||||
f"Сканирование Wi-Fi завершено за {time.time() - start_time:.2f} секунд, "
|
||||
f"найдено {len(aps)} уникальных сетей")
|
||||
return WifiScanResult(access_points=aps)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сканировании Wi-Fi: {str(e)}")
|
||||
return WifiScanResult(access_points=aps)
|
||||
|
||||
def add_note(self, ssid: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Добавляет заметку к сети Wi-Fi по SSID.
|
||||
|
||||
Args:
|
||||
ssid (str): SSID сети Wi-Fi.
|
||||
note (str): Текст заметки.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении заметки.
|
||||
"""
|
||||
try:
|
||||
saved_notes = self.storage.load_notes()
|
||||
if ssid not in saved_notes or saved_notes[ssid] is None:
|
||||
logger.info(f"Добавлена новая заметка для сети с SSID {ssid}")
|
||||
elif saved_notes[ssid] is not None:
|
||||
logger.error(f"Заметка для сети с SSID {ssid} уже существует")
|
||||
return False, f"Заметка для сети с SSID {ssid} уже существует"
|
||||
saved_notes[ssid] = note
|
||||
self.storage.save_notes(saved_notes)
|
||||
logger.info(f"Добавлена заметка '{note}' для сети с SSID {ssid}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при добавлении заметки для SSID {ssid}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
|
||||
def update_note(self, ssid: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Обновляет заметку сети Wi-Fi по SSID.
|
||||
|
||||
Args:
|
||||
ssid (str): SSID сети Wi-Fi.
|
||||
note (str): Новый текст заметки.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении заметки.
|
||||
"""
|
||||
try:
|
||||
saved_notes = self.storage.load_notes()
|
||||
if ssid not in saved_notes:
|
||||
logger.warning(f"Сеть с SSID {ssid} не найдена")
|
||||
return False, f"Сеть с SSID {ssid} не найдена"
|
||||
if saved_notes[ssid] is None:
|
||||
logger.error(f"Заметка для сети с SSID {ssid} не существует")
|
||||
return False, f"Заметка для сети с SSID {ssid} не существует"
|
||||
saved_notes[ssid] = note
|
||||
self.storage.save_notes(saved_notes)
|
||||
logger.info(f"Обновлена заметка '{note}' для сети с SSID {ssid}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении заметки для SSID {ssid}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
|
||||
def delete_note(self, ssid: str) -> Tuple[bool, Optional[str]]:
|
||||
"""Удаляет заметку сети Wi-Fi по SSID.
|
||||
|
||||
Args:
|
||||
ssid (str): SSID сети Wi-Fi.
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||
- Успех: (True, None).
|
||||
- Ошибка: (False, сообщение об ошибке).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при сохранении изменений.
|
||||
"""
|
||||
try:
|
||||
saved_notes = self.storage.load_notes()
|
||||
if ssid not in saved_notes:
|
||||
logger.warning(f"Сеть с SSID {ssid} не найдена")
|
||||
return False, f"Сеть с SSID {ssid} не найдена"
|
||||
if saved_notes[ssid] is None:
|
||||
logger.error(f"Заметка для сети с SSID {ssid} не существует")
|
||||
return False, f"Заметка для сети с SSID {ssid} не существует"
|
||||
saved_notes[ssid] = None
|
||||
self.storage.save_notes(saved_notes)
|
||||
logger.info(f"Удалена заметка для сети с SSID {ssid}")
|
||||
return True, None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении заметки для SSID {ssid}: {str(e)}")
|
||||
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||
0
SRV/storage/__init__.py
Normal file
0
SRV/storage/__init__.py
Normal file
183
SRV/storage/json_storage.py
Normal file
183
SRV/storage/json_storage.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
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-файле.
|
||||
|
||||
Предоставляет методы для создания, чтения и проверки данных пользователей,
|
||||
используя шифрование паролей с помощью bcrypt.
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str = "/etc/gonets/users.json"):
|
||||
"""Инициализирует хранилище пользователей.
|
||||
|
||||
Args:
|
||||
file_path (str, optional): Путь к JSON-файлу для хранения данных.
|
||||
По умолчанию "/etc/gonets/users.json".
|
||||
|
||||
Создаёт объект CryptContext для хеширования паролей и проверяет/создаёт
|
||||
файл, если он отсутствует.
|
||||
"""
|
||||
self.file_path = file_path
|
||||
self.pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
self._ensure_file_exists()
|
||||
|
||||
def _ensure_file_exists(self):
|
||||
"""Создаёт JSON-файл, если он не существует.
|
||||
|
||||
Создаёт родительские директории и файл с начальной структурой данных,
|
||||
устанавливая права доступа 600.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при создании файла или директории.
|
||||
"""
|
||||
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-файла.
|
||||
|
||||
Returns:
|
||||
dict: Словарь с данными пользователей и следующим ID.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: Если файл отсутствует (возвращает пустую структуру).
|
||||
Exception: Если произошла ошибка при чтении файла.
|
||||
"""
|
||||
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-файл.
|
||||
|
||||
Args:
|
||||
data (dict): Словарь с данными для записи (пользователи и next_id).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при записи в файл.
|
||||
"""
|
||||
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:
|
||||
"""Создаёт нового пользователя с хешированием пароля.
|
||||
|
||||
Args:
|
||||
user (UserCreate): Объект с данными нового пользователя (username, password, is_admin).
|
||||
|
||||
Returns:
|
||||
UserResponse: Объект с данными созданного пользователя (id, username, is_admin).
|
||||
|
||||
Raises:
|
||||
ValueError: Если пользователь с таким именем уже существует.
|
||||
Exception: Если произошла ошибка при записи данных.
|
||||
"""
|
||||
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]:
|
||||
"""Получает данные пользователя по имени.
|
||||
|
||||
Args:
|
||||
username (str): Имя пользователя для поиска.
|
||||
|
||||
Returns:
|
||||
Optional[dict]: Словарь с данными пользователя или None, если пользователь не найден.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при чтении файла.
|
||||
"""
|
||||
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:
|
||||
"""Проверяет соответствие введённого пароля хэшированному.
|
||||
|
||||
Args:
|
||||
plain_password (str): Введённый пользователем пароль.
|
||||
hashed_password (str): Хэшированный пароль из хранилища.
|
||||
|
||||
Returns:
|
||||
bool: True, если пароль верен, False в противном случае.
|
||||
"""
|
||||
return self.pwd_context.verify(plain_password, hashed_password)
|
||||
241
SRV/storage/session_storage.py
Normal file
241
SRV/storage/session_storage.py
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
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:
|
||||
"""Хранилище сессий пользователей в JSON-файле.
|
||||
|
||||
Предоставляет методы для создания, получения, удаления и очистки сессий с
|
||||
учётом срока действия.
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str = "/etc/gonets/sessions.json"):
|
||||
"""Инициализирует хранилище сессий.
|
||||
|
||||
Args:
|
||||
file_path (str, optional): Путь к JSON-файлу для хранения сессий.
|
||||
По умолчанию "/etc/gonets/sessions.json".
|
||||
|
||||
Создаёт файл и директории, если они отсутствуют, с правами доступа 600.
|
||||
"""
|
||||
self.file_path = file_path
|
||||
self._ensure_file_exists()
|
||||
|
||||
def _ensure_file_exists(self):
|
||||
"""Создаёт JSON-файл, если он не существует.
|
||||
|
||||
Создаёт родительские директории и файл с начальной структурой данных,
|
||||
устанавливая права доступа 600.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при создании файла или директории.
|
||||
"""
|
||||
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:
|
||||
"""Читает данные из JSON-файла.
|
||||
|
||||
Returns:
|
||||
Dict: Словарь с данными сессий.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при чтении файла.
|
||||
"""
|
||||
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):
|
||||
"""Записывает данные в JSON-файл.
|
||||
|
||||
Args:
|
||||
data (Dict): Словарь с данными сессий для записи.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при записи в файл.
|
||||
"""
|
||||
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.
|
||||
|
||||
Args:
|
||||
username (str): Имя пользователя, для которого создаётся сессия.
|
||||
token (str): Токен аутентификации.
|
||||
client_id (str): Идентификатор клиента.
|
||||
|
||||
Returns:
|
||||
str: Уникальный идентификатор сессии (session_id).
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при создании или записи сессии.
|
||||
|
||||
Note:
|
||||
Удаляет истекшие сессии перед созданием новой и устанавливает срок
|
||||
действия 30 минут.
|
||||
"""
|
||||
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.
|
||||
|
||||
Args:
|
||||
client_id (str): Идентификатор клиента.
|
||||
|
||||
Returns:
|
||||
Optional[Dict]: Словарь с данными последней активной сессии, включая
|
||||
"session_id", или None, если сессия не найдена.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при чтении или обработке данных.
|
||||
|
||||
Note:
|
||||
Очищает истекшие сессии перед поиском и возвращает сессию с самым
|
||||
поздним временем создания.
|
||||
"""
|
||||
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.
|
||||
|
||||
Args:
|
||||
session_id (str): Уникальный идентификатор сессии.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при чтении или записи данных.
|
||||
|
||||
Note:
|
||||
Очищает истекшие сессии перед удалением.
|
||||
"""
|
||||
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):
|
||||
"""Удаляет все истекшие сессии из хранилища.
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при чтении или записи данных.
|
||||
|
||||
Note:
|
||||
Проверяет срок действия каждой сессии и удаляет те, что истекли,
|
||||
записывая обновлённые данные.
|
||||
"""
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue