Версия 0.0.3

This commit is contained in:
parent 413e9c57be
commit 4b5c7358ac
189 changed files with 52063 additions and 2769 deletions

10
.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
.venv
.idea
__pycache__
.gigaide
.vscode
.wheels
.vendor
.python3-deb
SRV/storage/devices/devices.json
SRV/storage/wifi/wifi.json

View file

@ -1 +0,0 @@
debian/patches

View file

@ -1 +0,0 @@
series

View file

@ -1 +0,0 @@
2

View file

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

View file

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

View file

@ -1,2 +0,0 @@
1
2

View file

@ -1,10 +1,8 @@
import React from 'react'; import React from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom'; import { HashRouter } from 'react-router-dom';
import { NetworkService } from './src/api/request.ts';
import { useIpStore } from './src/store'; import { useIpStore } from './src/store';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useAdminStore } from './src/store/adminStore.ts';
import {useCheckAdmin} from './src/utils/checkAdmin.ts' import {useCheckAdmin} from './src/utils/checkAdmin.ts'
import { import {

View file

@ -21,7 +21,7 @@ function createWindow() {
win.show(); win.show();
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
win.loadURL('http://localhost:3000'); win.loadURL('https://localhost:8001');
win.webContents.openDevTools(); win.webContents.openDevTools();
} else { } else {
win.loadFile(path.join(__dirname, 'dist/index.html')); win.loadFile(path.join(__dirname, 'dist/index.html'));

View file

@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@iconify/icons-ic": "^1.2.13", "@iconify/icons-ic": "^1.2.13",
"@iconify/icons-ph": "^1.2.5",
"@iconify/react": "^5.2.0", "@iconify/react": "^5.2.0",
"@reduxjs/toolkit": "^2.6.0", "@reduxjs/toolkit": "^2.6.0",
"@xyflow/react": "^12.3.5", "@xyflow/react": "^12.3.5",

View file

@ -82,4 +82,79 @@ export class NetworkService {
return error.response; return error.response;
} }
} }
static async scanUser(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/devices');
} catch (error: any) {
return error.response;
}
}
static async scanWifi(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get('/subnet/wifi');
} catch (error: any) {
return error.response;
}
}
static async addNote(ipAddress: string, address:string, note: string,type:string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/subnet/${type}/${address}/note`, {
note: note
});
} catch (error: any) {
return error.response;
}
}
static async editNote(ipAddress: string, address:string, note: string,type:string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.put(`/subnet/${type}/${address}/note`, {
note: note
});
} catch (error: any) {
return error.response;
}
}
static async deleteNote(ipAddress: string, address:string, type:string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.delete(`/subnet/${type}/${address}/note`);
} catch (error: any) {
return error.response;
}
}
static async statusNetwork(ipAddress: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.get(`/subnet/wifi/status`);
} catch (error: any) {
return error.response;
}
}
static async connectNetwork(ipAddress: string, password:string, address: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/subnet/wifi/connect`, {
ssid: address,
password: password,
});
} catch (error: any) {
return error.response;
}
}
static async disconnectNetwork(ipAddress: string, ssid: string): Promise<AxiosResponse> {
try {
const axiosInstance = this.createAxiosInstance(ipAddress);
return await axiosInstance.post(`/subnet/wifi/disconnect`, {
ssid: ssid,
});
} catch (error: any) {
return error.response;
}
}
} }

View file

@ -0,0 +1,137 @@
import React, {useState} from 'react';
import styled, { css } from 'styled-components';
import { DeviceUser } from "../../../store/useDeviceStore";
import {buttonNames} from "../../../const/buttons/names";
import {ModalMap} from "../../modalwindows/windowsType";
import {IconComponent} from "../../../const/buttons/icons";
import {useModalContext} from "../../../store";
interface DeviceCardProps {
device: DeviceUser;
status: 'online' | 'offline';
}
const CardWrapper = styled.div<{ status: 'online' | 'offline' }>`
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
margin-bottom: 16px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
`;
const CardHeader = styled.div<{ status: 'online' | 'offline' }>`
background: ${({ status, theme }) =>
status === 'online' ? theme.successColor : theme.offlineColor};
color: white;
padding: 16px;
font-size: 18px;
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
`;
const CardContent = styled.div<{ isExpanded: boolean }>`
max-height: 0;
opacity: 0;
padding: 0 16px;
transition: all 0.3s ease;
color: #333;
font-size: 14px;
display: grid;
gap: 12px;
${({ isExpanded }) => isExpanded && css`
max-height: 500px;
opacity: 1;
padding: 16px;
`}
`;
const InfoRow = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
const Label = styled.span`
font-weight: 500;
color: #666;
`;
const Value = styled.span`
color: #333;
`;
const DeviceCard: React.FC<DeviceCardProps> = ({ device, status }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpanded = () => setIsExpanded(!isExpanded);
const { openModal } = useModalContext();
const openModalNote = (e: React.MouseEvent) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
currentNote: device.note || ''
});
};
return (
<CardWrapper status={status} onClick={toggleExpanded}>
<CardHeader status={status}>
<p>
MAC: {device.mac}
</p>
<span onClick={openModalNote}>
<IconComponent name={buttonNames[16]} width="28" height="28" setColor={'white'} />
</span>
</CardHeader>
<CardContent isExpanded={isExpanded}>
<InfoRow>
<Label>Имя:</Label>
<Value>{device.hostname || device.ip}</Value>
</InfoRow>
<InfoRow>
<Label>Производитель:</Label>
<Value>{device.vendor || 'Неизвестно'}</Value>
</InfoRow>
<InfoRow>
<Label>IP адрес:</Label>
<Value>{device.ip}</Value>
</InfoRow>
{device.open_ports?.length > 0 && (
<InfoRow>
<Label>Открытые порты:</Label>
<Value>{device.open_ports.join(', ')}</Value>
</InfoRow>
)}
<InfoRow>
<Label>Последняя активность:</Label>
<Value>{device.last_seen}</Value>
</InfoRow>
<InfoRow>
<Label>Заметка:</Label>
<Value>{device.note || '-'}</Value>
</InfoRow>
</CardContent>
</CardWrapper>
);
};
export default DeviceCard;

View file

@ -0,0 +1,130 @@
import React, {useEffect, useRef, useState} from 'react';
import styled from "styled-components";
import {buttonNames} from "../../../const/buttons/names";
import {IconComponent} from "../../../const/buttons/icons";
type Option = {
value: number,
label: string,
}
interface DropdownTypeProps {
options: Option[];
label?: string;
onChange?: (selectedItem: Option) => void;
value: Option | null;
}
const DropdownWrapper = styled.div`
position: absolute;
top: 18px;
right:20px;
display: inline-block;
background-color: ${({theme}) => theme.background};
padding: 12px 16px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
`
const Label = styled.p`
color: ${({theme}) => theme.textColor};
font-size: 16px;
`
const Dropdown = styled.div`
background-color: ${({theme}) => theme.primary};
border-radius: 15px;
overflow: hidden;
transition: all .3s ease;
cursor: pointer;
z-index: 10;
margin-top: 6px;
`
const DropdownHeader = styled.div`
display: flex;
justify-content: space-between;
gap: 14px;
padding: 12px 24px;
font-size: 16px;
align-items: center;
height: 100%;
min-width: 200px;
`
const DropdownContent = styled.div<{$isOpen: boolean}>`
max-height: ${({$isOpen}) => $isOpen ? '500px' : '0'};
opacity: ${({$isOpen}) => $isOpen ? 1 : 0};
transition: all .3s ease;
display: flex;
flex-direction: column;
overflow: hidden;
`
const DropdownRow = styled.button`
padding: 12px 24px;
background-color: transparent;
text-align: start;
font-size: 16px;
border: none;
width: 100%;
color: ${({theme}) => theme.textColor};
transition: all .3s ease;
cursor: pointer;
&:hover {
background-color: ${({theme}) => theme.background};
}
`
const DropdownArrow = styled.div<{ $isOpen: boolean }>`
transition: transform 0.3s ease;
transform: rotate(${({ $isOpen }) => ($isOpen ? "90deg" : "0deg")});
`;
const DropdownType = ({options, value, onChange, label}: DropdownTypeProps) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
function toggleOpen() {
setIsOpen((prev) => !prev);
}
function handleSelect(option: Option) {
onChange?.(option);
setIsOpen(false);
}
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<DropdownWrapper ref={dropdownRef}>
<Label>{label}</Label>
<Dropdown>
<DropdownHeader onClick={toggleOpen}>
<p>{value?.label || 'Select an option'}</p>
<DropdownArrow $isOpen={isOpen}>
<IconComponent name={buttonNames[6]} />
</DropdownArrow>
</DropdownHeader>
<DropdownContent $isOpen={isOpen}>
{options.map((option, i) => (
<DropdownRow key={i} onClick={() => handleSelect(option)}>
{option.label}
</DropdownRow>
))}
</DropdownContent>
</Dropdown>
</DropdownWrapper>
);
};
export default DropdownType;

View file

@ -9,15 +9,15 @@ const Button = styled.button.withConfig({
border: none; border: none;
padding: ${(props) => padding: ${(props) =>
props.size === "small" props.size === "small"
? "4px 8px" ? "10px 28px"
: props.size === "big" : props.size === "big"
? "10px 20px" ? "10px 20px"
: "6px 10px"}; // По умолчанию : "12px 20px"}; // По умолчанию
border-radius: 8px; border-radius: 8px;
color: white; color: white;
font-size: ${(props) => font-size: ${(props) =>
props.size === "small" props.size === "small"
? "12px" ? "14px"
: props.size === "big" : props.size === "big"
? "16px" ? "16px"
: "14px"}; // По умолчанию : "14px"}; // По умолчанию
@ -30,7 +30,7 @@ const Button = styled.button.withConfig({
? 'row-reverse' ? 'row-reverse'
: 'row'}; : 'row'};
gap: 10px; gap: 10px;
transition: all .3s ease;
&:hover{ &:hover{
background-color: ${(props) => props.$hoverсolor}; background-color: ${(props) => props.$hoverсolor};
} }

View file

@ -5,7 +5,7 @@ type Props = {
placeholderText: string; placeholderText: string;
id: string; id: string;
value?: string; value?: string;
onChange?: (identifier: string, value: string) => void; onChange?: (value: string) => void;
onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void; onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
type?: string; type?: string;
multiple?: boolean; multiple?: boolean;
@ -33,7 +33,7 @@ const StyledInput = styled.input`
border: 1px solid #adadad; border: 1px solid #adadad;
} }
width: 100%; width: 100%;
font-size: 14px; font-size: 18px;
color: ${props=>props.theme.textColor} color: ${props=>props.theme.textColor}
`; `;
@ -41,7 +41,7 @@ const Input = React.forwardRef<HTMLInputElement, Props>(({ placeholderText, id,
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target; const { value } = e.target;
if (onChange) { if (onChange) {
onChange(id, value); onChange(value);
} }
}; };

View file

@ -2,19 +2,21 @@ import React, { useEffect, useRef } from 'react';
interface SignalSVGProps { interface SignalSVGProps {
signal: number; // Принимаем signal как пропс signal: number; // Принимаем signal как пропс
color: string;
size: string;
} }
const SignalSVG: React.FC<SignalSVGProps> = ({ signal }) => { const SignalSVG: React.FC<SignalSVGProps> = ({ signal, color, size }) => {
const svgRef = useRef<SVGSVGElement | null>(null); const svgRef = useRef<SVGSVGElement | null>(null);
useEffect(() => { useEffect(() => {
// Устанавливаем цвета в зависимости от signal // Устанавливаем цвета в зависимости от signal
const colors = { const colors = {
'--st1-color': signal >= 1 ? 'white' : 'black', '--st1-color': signal >= 1 ? color : 'black',
'--st2-color': signal >= 2 ? 'white' : 'black', '--st2-color': signal >= 2 ? color : 'black',
'--st3-color': signal >= 3 ? 'white' : 'black', '--st3-color': signal >= 3 ? color : 'black',
'--st4-color': signal >= 4 ? 'white' : 'black', '--st4-color': signal >= 4 ? color : 'black',
'--st5-color': signal === 5 ? 'white' : 'black', '--st5-color': signal === 5 ? color : 'black',
}; };
// Применяем стили к SVG // Применяем стили к SVG
@ -35,8 +37,8 @@ const SignalSVG: React.FC<SignalSVGProps> = ({ signal }) => {
y="0px" y="0px"
viewBox="0 0 296 235" viewBox="0 0 296 235"
xmlSpace="preserve" xmlSpace="preserve"
width="20px" // Фиксированная ширина width={size} // Фиксированная ширина
height="20px" height={size}
> >
<style type="text/css"> <style type="text/css">
{` {`

View file

@ -0,0 +1,109 @@
import React from 'react';
import { DeviceUser } from '../../../store/useDeviceStore';
interface TableScannerProps {
devices: DeviceUser[];
}
import styled from 'styled-components';
import {IconComponent} from "../../../const/buttons/icons";
import {buttonNames} from "../../../const/buttons/names";
import {ModalMap} from "../../modalwindows/windowsType";
import {useModalContext} from "../../../store";
const TableContainer = styled.div`
width: 100%;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
background: ${({ theme }) => theme.primary};
margin: 24px 0;
`;
const StyledTable = styled.table`
width: 100%;
border-collapse: separate;
border-spacing: 0;
`;
const TableHeaderCell = styled.th<{ sortable?: boolean }>`
padding: 16px 24px;
text-align: left;
font-weight: 600;
font-size: 14px;
color: ${({ theme }) => theme.textSecondary};
background: ${({ theme }) => theme.primary};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
`;
const TableRow = styled.tr<{ clickable?: boolean }>`
transition: all 0.2s ease;
&:hover {
background-color: ${({ theme }) => theme.background};
}
`;
const TableCell = styled.td`
padding: 16px 24px;
font-size: 14px;
color: ${({ theme }) => theme.text};
border-bottom: 1px solid ${({ theme }) => theme.borderColor};
transition: all 0.2s ease;
`;
const StatusBadge = styled.span<{ status:string }>`
width: 10px;
height: 30px;
background-color: ${({theme, status}) => (status==='online' ? theme.successColor : theme.offlineColor)};
display: inline-block;
border-radius:4px;
`
const TableScanner = ({devices}:TableScannerProps) => {
const { openModal } = useModalContext();
const openModalNote = (e: React.MouseEvent, device:DeviceUser ) => {
e.stopPropagation();
openModal(ModalMap['6'], {
address: device.mac,
currentNote: device.note || ''
});
};
return (
<TableContainer>
<StyledTable>
<thead>
<tr>
<TableHeaderCell></TableHeaderCell>
<TableHeaderCell>Имя</TableHeaderCell>
<TableHeaderCell>Производитель</TableHeaderCell>
<TableHeaderCell>IP адрес</TableHeaderCell>
<TableHeaderCell>Открытые порты</TableHeaderCell>
<TableHeaderCell>Последняя активность</TableHeaderCell>
<TableHeaderCell>Заметка</TableHeaderCell>
<TableHeaderCell></TableHeaderCell>
</tr>
</thead>
<tbody>
{devices.map((item, i) => (
<TableRow key={i}>
<TableCell><StatusBadge status={item.status}></StatusBadge></TableCell>
<TableCell>{item.hostname || item.ip}</TableCell>
<TableCell>{item.vendor}</TableCell>
<TableCell>{item.ip}</TableCell>
<TableCell>{item.open_ports.join(', ') || '-'}</TableCell>
<TableCell>{item.last_seen}</TableCell>
<TableCell>{item.note || '-'}</TableCell>
<TableCell>
<span onClick={(e)=>openModalNote(e, item)}>
<IconComponent name={buttonNames[16]} width="26" height="26" />
</span>
</TableCell>
</TableRow>
))}
</tbody>
</StyledTable>
</TableContainer>
);
};
export default TableScanner;

View file

@ -184,7 +184,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
placeholderText={data.ipData.ipAdress} placeholderText={data.ipData.ipAdress}
id="newIp" id="newIp"
value={ipData.newIp} value={ipData.newIp}
onChange={(id, value) => handleChange(id, value)} onChange={(value) => handleChange('newIp', value)}
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}} style={{fontSize: '12px', padding: '4px 10px'}}
@ -203,7 +203,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
placeholderText={data.ipData.ipGate} placeholderText={data.ipData.ipGate}
id="newGate" id="newGate"
value={ipData.newGate} value={ipData.newGate}
onChange={(id, value) => handleChange(id, value)} onChange={(value) => handleChange('newGate', value)}
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}} style={{fontSize: '12px', padding: '4px 10px'}}
@ -221,7 +221,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
placeholderText={data.ipData.ipDns.join(', ')} placeholderText={data.ipData.ipDns.join(', ')}
id="newDns" id="newDns"
value={ipData.newDns.join(', ')} value={ipData.newDns.join(', ')}
onChange={(id, value) => handleChange(id, value)} onChange={( value) => handleChange('newDns', value)}
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}} style={{fontSize: '12px', padding: '4px 10px'}}

View file

@ -73,7 +73,7 @@ export const NetWorkNode = ({ data }: CustomNodeProps) => {
{data.networkData.type == 'WIFI' && data.networkData.state === "Подключено" ? ( {data.networkData.type == 'WIFI' && data.networkData.state === "Подключено" ? (
<Status> <Status>
<SignalSVG signal={signal} /> <SignalSVG signal={signal} color={'white'} size={'20px'} />
</Status> </Status>
) : ( ) : (
<div></div> <div></div>

View file

@ -18,7 +18,7 @@ export const Network = styled.div<Props>`
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
visibility: visible; visibility: visible;
overflow: hidden;
p{ p{
margin: 0; margin: 0;
} }
@ -29,8 +29,6 @@ export const TitleContainer = styled.div<Props>`
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
background: ${(props) => props.$backgroundcolor}; background: ${(props) => props.$backgroundcolor};
border-top-left-radius: 8px;
border-top-right-radius: 8px;
color: white; color: white;
padding: 15px; padding: 15px;
height: ${(props) => props.$height ? props.$height : 'unset'}; height: ${(props) => props.$height ? props.$height : 'unset'};

View file

@ -0,0 +1,84 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
import {useIpStore} from "../../../store";
interface Props {
address: string;
onClose: () => void;
}
export const ConnectNetwork = ({address, onClose}: Props) => {
const [password, setPassword] = useState('');
const theme = useTheme();
const { ipAddress } = useIpStore();
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSave = async () => {
try {
setIsLoading(true);
setError('')
const res = await NetworkService.connectNetwork(ipAddress, password, address);
if(res.status === 200) {
onClose()
}else{
setError(res.data.detail)
}
} catch (error) {
console.error('Error connection:', error);
setError('Ошибка подключения к сети')
}finally {
setIsLoading(false);
}
};
return (
<ModalContainer style={{ height: '230px' }}>
<ModalTitle>
<h3>Подключение к сети</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="password"
placeholderText='Введите пароль...'
value={password}
id='deviceNote'
onChange={(value) => setPassword(value)}
/>
<p style={{ color: 'red', marginLeft: '10px', fontSize: '14px', textAlign: 'right' }}>
{error}
</p>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title="Отмена"
onClick={onClose}
/>
<MainButton
type="button"
size="small"
color={theme.successColor}
hoverColor={theme.successColor}
title='Подключиться'
onClick={handleSave}
/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText='Попытка подключения к сети'/>}
</ModalContainer>
);
};

View file

@ -0,0 +1,82 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {useIpStore} from "../../../store";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
interface Props {
address: string;
onClose: () => void;
currentNote?: string;
type: string
}
export const NoteDevice = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { devices, setDevices } = useNetworkStore();
const handleSave = async () => {
try {
if (!currentNote && note.trim() !== '') {
await NetworkService.addNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
await NetworkService.editNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() === '') {
await NetworkService.deleteNote(ipAddress, address, type);
}else {
onClose();
return;
}
const updatedDevices = devices.map(device =>
device.mac === address
? { ...device, note: note.trim() }
: device
);
setDevices(updatedDevices);
onClose();
} catch (error) {
console.error('Error saving note:', error);
}
};
return (
<ModalContainer style={{ height: '250px' }}>
<ModalTitle>
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText='Введите заметку...'
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
/>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title="Отмена"
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title='Сохранить'
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -0,0 +1,82 @@
// components/modalContainers/NoteDevice.tsx
import { ModalContainer, ModalTitle, ModalContent, ButtonContainer } from "../style";
import { MainButton } from "../../UI/buttons/mainButton";
import Input from "../../UI/inputs/input";
import { useState } from "react";
import {NetworkService} from "../../../api/request";
import useNetworkStore from "../../../store/useDeviceStore";
import {useIpStore} from "../../../store";
interface Props {
address: string;
onClose: () => void;
currentNote?: string;
type: string
}
export const NoteWifi = ({ address, onClose, currentNote, type}: Props) => {
const [note, setNote] = useState(currentNote || '');
const { ipAddress } = useIpStore();
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const handleSave = async () => {
try {
if (!currentNote && note.trim() !== '') {
await NetworkService.addNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() !== '' && note !== currentNote) {
await NetworkService.editNote(ipAddress, address, note, type);
}else if (currentNote && note.trim() === '') {
await NetworkService.deleteNote(ipAddress, address, type);
}else {
onClose();
return;
}
const updatedNetworks = wifiNetworks.map(network =>
network.ssid === address
? { ...network, note: note.trim() }
: { ...network }
);
setWifiNetworks(updatedNetworks);
onClose();
} catch (error) {
console.error('Error saving note:', error);
}
};
return (
<ModalContainer style={{ height: '220px' }}>
<ModalTitle>
<h3>{currentNote ? 'Редактирование заметки' : 'Добавление заметки'}</h3>
</ModalTitle>
<ModalContent style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Input
type="text"
placeholderText='Введите заметку...'
value={note}
id='deviceNote'
onChange={(value) => setNote(value)}
/>
</ModalContent>
<ButtonContainer>
<MainButton
type="button"
size="small"
title="Отмена"
onClick={onClose}
/>
<MainButton
type="button"
size="small"
title='Сохранить'
onClick={handleSave}
/>
</ButtonContainer>
</ModalContainer>
);
};

View file

@ -39,7 +39,7 @@ export const AddDevice = ({ onSave, onClose }: Props) => {
placeholderText='Введите адрес узла...' placeholderText='Введите адрес узла...'
id="newIp" id="newIp"
value={ipAddress} value={ipAddress}
onChange={(id, value) => setIpAddress(value)} onChange={(value) => setIpAddress(value)}
/> />
{error && ( {error && (
<p style={{ color: 'red', marginTop: '5px' }}> <p style={{ color: 'red', marginTop: '5px' }}>

View file

@ -0,0 +1,54 @@
import { ModalContainer, ModalTitle, ModalContent, ModalText, ButtonContainer } from "../style"
import { useModalContext } from "../../../store"
import { useIpStore } from "../../../store"
import { MainButton } from "../../UI/buttons/mainButton"
import {useState} from "react";
import {NetworkService} from "../../../api/request";
import RouterLoader from "../../UI/routerLoader";
import {useTheme} from "styled-components";
type Props={
address: string,
onClose: () => void
}
export const DisconnectConfirm = ({address, onClose}: Props)=>{
const { ipAddress } = useIpStore();
const theme = useTheme()
const [isLoading, setIsLoading] = useState(false);
const handleDisconnect = async () => {
try {
setIsLoading(true);
const res = await NetworkService.disconnectNetwork(ipAddress, address);
if(res.status === 200) {
onClose()
}
} catch (error) {
console.error('Error connection:', error);
}finally {
setIsLoading(false);
}
};
return(
<ModalContainer>
<ModalTitle>
<h3>Отключение от Wi-Fi сети</h3>
</ModalTitle>
<ModalContent>
<ModalText>
<h3>Подтверждение отключения</h3>
<p>Вы собираетесь отключиться от сети "{address}". Все активные подключения будут прерваны.</p>
</ModalText>
</ModalContent>
<ButtonContainer>
<MainButton title="Отменить" onClick={onClose} size="small"/>
<MainButton title="Отключиться" color={theme.errorColor} hoverColor={theme.errorColor} onClick={handleDisconnect} size="small"/>
</ButtonContainer>
{isLoading && <RouterLoader loaderText='Отключение от сети'/>}
</ModalContainer>
)
}

View file

@ -110,6 +110,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
try { try {
const result = await NetworkService.saveIp(ipAddress, data); const result = await NetworkService.saveIp(ipAddress, data);
if (result.status === 200) { if (result.status === 200) {
if(ipAddress!=='localhost'){
setIpAddress(ipData.newIp) setIpAddress(ipData.newIp)
const updatedDevices = devices.map(device => const updatedDevices = devices.map(device =>
@ -118,6 +119,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
: device : device
); );
useDevicesStore.setState({ devices: updatedDevices }); useDevicesStore.setState({ devices: updatedDevices });
}
onClose() onClose()
} }

View file

@ -87,7 +87,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
placeholderText='Введите пароль администратора...' placeholderText='Введите пароль администратора...'
id="password" id="password"
value={user.password} value={user.password}
onChange={(id, value) => handleChange(id, value)} onChange={(value) => handleChange('password', value)}
/> />
{error && ( {error && (

View file

@ -3,6 +3,10 @@ import { IpConfirm, ConnectionError, AddDevice, RightsConfirm } from "./modalCon
import { ModalMap } from "./windowsType"; import { ModalMap } from "./windowsType";
import Loader from "../UI/loader"; import Loader from "../UI/loader";
import { ModalWindow } from "./style"; import { ModalWindow } from "./style";
import {NoteDevice} from "./modalContainers/NoteDevice";
import { NoteWifi } from "./modalContainers/NoteWifi";
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
type Props = { type Props = {
isOpen: boolean, isOpen: boolean,
@ -35,7 +39,34 @@ export default function WindowComponent({ isOpen, type, data }: Props) {
message={data.message} message={data.message}
/> />
} }
{type === ModalMap['6'] && (
<NoteDevice
type={'devices'}
address={data.address}
currentNote={data.currentNote}
onClose={closeLoader}
/>
)}
{type === ModalMap['7'] && (
<NoteWifi
type={'wifi'}
address={data.address}
currentNote={data.currentNote}
onClose={closeLoader}
/>
)}
{type === ModalMap['8'] && (
<ConnectNetwork
address={data.address}
onClose={closeLoader}
/>
)}
{type === ModalMap['9'] && (
<DisconnectConfirm
address={data.address}
onClose={closeLoader}
/>
)}
{type === ModalMap['4'] && ( {type === ModalMap['4'] && (
<AddDevice onSave={data?.onSave} onClose={closeLoader} /> <AddDevice onSave={data?.onSave} onClose={closeLoader} />
)} )}

View file

@ -24,7 +24,7 @@ export const ModalContainer = styled.div<{height?: string}>`
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
gap: 15px; gap: 15px;
height: ${(props) => props.height ? props.height : '200px'}; height: ${(props) => props.height ? props.height : '230px'};
width: 520px; width: 520px;
` `
@ -66,5 +66,5 @@ export const ButtonContainer = styled.div`
justify-content: flex-end; justify-content: flex-end;
display: flex; display: flex;
gap: 10px; gap: 10px;
padding: 20px; padding: 10px 20px 20px;
` `

View file

@ -5,5 +5,9 @@ export const ModalMap: ModalType = {
'2': 'loader', '2': 'loader',
'3': 'connectionError', '3': 'connectionError',
'4': 'addDevice', '4': 'addDevice',
'5': 'sudo' '5': 'sudo',
'6': 'noteDevice',
'7': 'noteWifi',
'8': 'connectNetwork',
'9': 'disconnectConfirm',
} }

View file

@ -101,7 +101,6 @@ export const CreateUser = () => {
if (result.status === 200) { window.location.reload() } if (result.status === 200) { window.location.reload() }
} catch (err) { } catch (err) {
console.error('Error:', err); console.error('Error:', err);
} finally {
} }
} }
@ -146,7 +145,7 @@ export const CreateUser = () => {
placeholderText='Введите пароль...' placeholderText='Введите пароль...'
id="password" id="password"
value={user.password} value={user.password}
onChange={(id, value) => handleChange(id, value)} onChange={(value) => handleChange('password', value)}
/> />
<Input <Input
@ -154,7 +153,7 @@ export const CreateUser = () => {
placeholderText='Повторите пароль...' placeholderText='Повторите пароль...'
id="repeatPassword" id="repeatPassword"
value={repeatPassword} value={repeatPassword}
onChange={(id, value) => handleChange(id, value)} onChange={(value) => handleChange('repeatPassword', value)}
style={{ marginTop: '10px' }} style={{ marginTop: '10px' }}
/> />
{!passwordsMatch && ( {!passwordsMatch && (

View file

@ -0,0 +1,73 @@
import React from 'react';
import styled from 'styled-components';
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: ()=>void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
const activeDevices = devices.filter(device => device.status === 'online');
const inactiveDevices = devices.filter(device => device.status !== 'online');
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы подсети'}
</ScanBtn>
{devices.length > 0 ? (
<div style={{
display: 'flex',
width: '70%',
gap: '20px',
}}>
{/* Колонка активных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#4CAF50', marginBottom:'12px' }}>Активные устройства ({activeDevices.length})</h3>
{activeDevices.map((device, index) => (
<DeviceCard
key={index}
status="online"
device={device}
/>
))}
</div>
{/* Колонка неактивных устройств */}
<div style={{ flex: 1 }}>
<h3 style={{ color: '#ababab', marginBottom:'12px' }}>Неактивные устройства ({inactiveDevices.length})</h3>
{inactiveDevices.map((device, index) => (
<DeviceCard
key={index}
status="offline"
device={device}
/>
))}
</div>
</div>
) : scanInitiated ? (
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
) : null}
</>
);
};
export default ListUsers;

View file

@ -0,0 +1,45 @@
import React from 'react';
import styled from 'styled-components';
import DeviceCard from "../../../../../UI/Dropdown/DeviceCard";
import {DeviceUser} from "../../../../../../store/useDeviceStore";
import TableScanner from "../../../../../UI/tables/TableScanner";
const ScanBtn = styled.button`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: .4s ease;
&:hover{
transform: scale(1.03);
}
`
interface ListUsersProps {
loading: boolean;
handleScan: ()=>void;
devices: DeviceUser[];
scanInitiated: boolean;
}
const ListUsers = ({loading, handleScan, devices, scanInitiated}: ListUsersProps) => {
return (
<>
<ScanBtn onClick={handleScan} disabled={loading}>
{devices.length > 0 ? 'Обновить список' : 'Сканировать узлы подсети'}
</ScanBtn>
{devices.length > 0 ? (
<TableScanner devices={devices} />
) : scanInitiated ? (
<p style={{ marginTop: '20px' }}>Устройства не найдены</p>
) : null}
</>
);
};
export default ListUsers;

View file

@ -0,0 +1,191 @@
import { useIpStore } from "../../../../../store";
import styled, { keyframes } from "styled-components";
import { useState } from "react";
import { NetworkService } from "../../../../../api/request";
import ListUsers from "./modules/ListUsers";
import TableUsers from "./modules/TableUsers";
import DropdownType from "../../../../UI/Dropdown/DropdownType";
import useNetworkStore from "../../../../../store/useDeviceStore";
type Option ={
value: number,
label: string,
}
// Анимации
const ring1 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
`;
const ring2 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
`;
const ring3 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
const pulse = keyframes`
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:${({scanInitiated, loading})=>(scanInitiated && !loading ? 'start' : 'center')};
height: 100%;
width: 100%;
background: ${(props) => props.theme.background};
padding: 50px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
position: relative;
`;
const TitleWrapper = styled.h2`
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
width: max-content;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: ${({scanInitiated,loading})=>(scanInitiated && !loading ? 'scale(0)' : 'scale(1)')};
span {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.accent};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center {
width: 60px;
height: 60px;
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
export default function UserScanner() {
const { ipAddress } = useIpStore();
const [loading, setLoading] = useState(false);
const { devices, setDevices } = useNetworkStore();
const [scanInitiated, setScanInitiated] = useState(devices.length > 0);
// Сканирование
const handleScan = async () => {
try {
setLoading(true);
setScanInitiated(true);
const res = await NetworkService.scanUser(ipAddress);
console.log(res);
setDevices(res.data.devices || []);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
setDevices([])
} finally {
setLoading(false);
}
};
// Способ отображения
const viewOptions: Option[] = [
{ value: 1, label: 'Таблица' },
{ value: 2, label: 'Список' },
];
const [selectedView, setSelectedView] = useState<Option>(viewOptions[0]);
const handleViewChange = (option: Option) => {
setSelectedView(option);
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
{(!scanInitiated || loading) && (<TitleWrapper>Сканирование узлов в подсети</TitleWrapper>)}
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center"></span>
<span className="ring1"></span>
<span className="ring2"></span>
<span className="ring3"></span>
</UserLocate>
{scanInitiated && !loading && (
<DropdownType options={viewOptions} value={selectedView} onChange={handleViewChange} label="Способ отображения" />
)}
{loading ? (
<LoadingText>
Сканирование узлов в подсети<ScanningText />
</LoadingText>
) : selectedView.value == 1 ? (
<TableUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
):(
<ListUsers loading={loading} handleScan={handleScan} devices={devices} scanInitiated={scanInitiated} />
)}
</Wrapper>
);
}

View file

@ -0,0 +1,199 @@
import styled, { keyframes } from "styled-components";
import React, { useState } from "react";
import { NetworkService } from "../../../../../api/request";
import WifiDot, {WifiNetworkScanner} from "./modules/WifiDot";
import ModalWifiScaner from "./modules/ModalWifiScaner";
import useNetworkStore from "../../../../../store/useDeviceStore";
import {useIpStore} from "../../../../../store";
// Анимации
const ring1 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.2; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.12; }
`;
const ring2 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.15; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.08; }
`;
const ring3 = keyframes`
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
50% { transform: translate(-50%, -50%) scale(1.1); opacity: 0.1; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 0.04; }
`;
const dotAnimation = keyframes`
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
`;
const pulse = keyframes`
0% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.1); }
100% { transform: translate(-50%, -50%) scale(1); }
`;
// Стили
const Wrapper = styled.div<{ scanInitiated: boolean, loading: boolean}>`
display: flex;
flex-direction: column;
align-items: center;
justify-content:center ;
height: 100%;
width: 100%;
position: relative;
background: ${(props) => props.theme.background};
padding: 50px;
box-sizing: border-box;
gap: 24px;
transition: justify-content .4s ease;
overflow-y: auto;
overflow-x: hidden;
`;
const TitleWrapper = styled.h2`
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
font-size: 32px;
`
const UserLocate = styled.div<{ loading: boolean, scanInitiated: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
position: relative;
transition: .4s ease;
transform: scale(1);
.circel {
position: absolute;
display: inline-block;
background: ${(props) => props.theme.scannerColor};
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 50%;
&.center {
width: 60px;
height: 60px;
animation: ${({ loading }) => (loading ? pulse : "none")} 2s ease infinite;
}
&.ring1 {
opacity: 0.12;
width: 250px;
height: 250px;
animation: ${({ loading }) => (loading ? ring1 : "none")} 4s linear infinite;
}
&.ring2 {
opacity: 0.08;
width: 400px;
height: 400px;
animation: ${({ loading }) => (loading ? ring2 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.1s" : "0")};
}
&.ring3 {
opacity: 0.04;
width: 550px;
height: 550px;
animation: ${({ loading }) => (loading ? ring3 : "none")} 4s linear infinite;
animation-delay: ${({ loading }) => (loading ? "0.2s" : "0")};
}
}
`;
const LoadingText = styled.div`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
`;
const ScanningText = styled.span`
&::after {
content: '.';
animation: ${dotAnimation} 1.5s infinite;
}
`;
const ScanBtn = styled.button<{scanInitiated: boolean}>`
position: relative;
padding: 14px 22px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
transition: transform .4s ease;
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")};
&:hover{
transform: ${({scanInitiated}) => (scanInitiated ? "translateY(240px)" : "translateY(0)")} scale(1.03);
}
`
export function WifiScanner() {
const { ipAddress } = useIpStore();
const [loading, setLoading] = useState(false);
const [scanInitiated, setScanInitiated] = useState(false);
const { wifiNetworks, setWifiNetworks } = useNetworkStore();
const [currentNetwork, setCurrentNetwork] = useState<WifiNetworkScanner | null>(null);
const handleScan = async () => {
setWifiNetworks([]);
setCurrentNetwork(null)
setLoading(true);
setScanInitiated(true);
try {
const res = await NetworkService.scanWifi(ipAddress);
const networks = res.data.access_points || [];
setWifiNetworks(networks);
} catch (err) {
console.error("Ошибка сканирования сети:", err);
setWifiNetworks([]);
} finally {
setLoading(false);
}
};
return (
<Wrapper scanInitiated={scanInitiated} loading={loading}>
<TitleWrapper>Сканирование точек доступа Wi-Fi</TitleWrapper>
<UserLocate loading={loading} scanInitiated={scanInitiated}>
<span className="center circel"></span>
<span className="ring1 circel"></span>
<span className="ring2 circel"></span>
<span className="ring3 circel"></span>
{wifiNetworks.map((network) => (
<WifiDot
key={network.bssid}
network={network}
onSelect={setCurrentNetwork}
/>
))}
{currentNetwork && (
<ModalWifiScaner
onClose={() => setCurrentNetwork(null)}
network={currentNetwork}
/>
)}
</UserLocate>
{loading ? (
<LoadingText>
Сканирование точек доступа <ScanningText/>
</LoadingText>
) : (
<ScanBtn onClick={handleScan} disabled={loading} scanInitiated={wifiNetworks.length > 0 }>
{wifiNetworks.length > 0 ? 'Обновить список' : 'Сканировать точки доступа'}
</ScanBtn>
)}
</Wrapper>
);
}

View file

@ -0,0 +1,225 @@
import React, {useEffect, useState} from 'react';
import styled from 'styled-components';
import {WifiNetworkScanner} from "./WifiDot";
import {IconComponent} from "../../../../../../const/buttons/icons";
import {buttonNames} from "../../../../../../const/buttons/names";
import {ModalMap} from "../../../../../modalwindows/windowsType";
import {useIpStore, useModalContext} from "../../../../../../store";
import {Status} from "../../../../../customeNodes/nodes/networkInterface/networkNode/style";
import SignalSVG from "../../../../../UI/signalSvg";
import { useTheme } from 'styled-components';
import useNetworkStore, {DeviceUser} from "../../../../../../store/useDeviceStore";
import {NetworkService} from "../../../../../../api/request";
const Modal = styled.div`
display: flex;
justify-content: center;
flex-direction: column;
background-color: ${(props) => props.theme.secondary};
border-radius: 24px;
padding: 32px 28px;
bottom: -40%;
left: 220px;
position: absolute;
z-index: 20;
min-width: 300px;
&:hover .close-btn{
opacity: 1;
visibility: visible;
}
`
const CloseBtn = styled.div`
position: absolute;
top: -20px;
right: -20px;
cursor: pointer;
background-color: ${(props) => props.theme.primary};
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
transition: all .4s ease;
opacity: 0;
visibility: visible;
&:hover {
background-color: ${(props) => props.theme.hoverBtn};
}
`
const ModalTitle = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
font-size: 20px;
line-height: 1;
margin-bottom: 32px;
h4{
font-weight: normal;
}
.functionalIcons{
display: flex;
justify-content: center;
align-items: center;
gap: 18px;
}
`
const ModalContent = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
justify-content: space-between;
align-items: center;
width: 100%;
`
const ModalRow = styled.div`
display: flex;
justify-content: space-between;
align-items: start;
gap: 12px;
width: 100%;
font-size: 14px;
h6{
margin: 0;
padding: 0;
font-size: 1em;
}
p{
max-width:180px;
text-align: right;
}
`
const ModalConnect = styled.button`
color: ${(props) => props.theme.primary};
padding: 10px 12px;
font-size: 18px;
background-color: ${(props) => props.theme.accent};
border-radius: 12px;
margin-top: 4px;
border: none;
outline: none;
width: 100%;
cursor: pointer;
transition: .4s ease;
&:hover{
background-color: ${(props) => props.theme.accentHover};
}
`
const ModalWifiScaner = ({ network,onClose }: { network: WifiNetworkScanner, onClose: ()=>void }) => {
const { openModal } = useModalContext();
const { ipAddress } = useIpStore();
const theme = useTheme()
const [signal, setSignal] = useState(1);
const { wifiNetworks } = useNetworkStore();
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const getStatus = async () => {
try {
const response = await NetworkService.statusNetwork(ipAddress);
const { status, ssid } = response.data;
setIsConnected(
!!network?.ssid &&
ssid === network.ssid &&
status === 'connected'
);
} catch (error) {
setIsConnected(false);
}
};
getStatus();
}, [network?.ssid, ipAddress]);
const currentNetwork = wifiNetworks.find(n => n.bssid === network.bssid) || network;
const openModalNote = (e: React.MouseEvent) => {
e.stopPropagation();
openModal(ModalMap['7'], {
address: currentNetwork.ssid,
currentNote: currentNetwork.note || ''
});
};
const openModalNetwork= (e: React.MouseEvent) => {
e.stopPropagation();
if(isConnected){
openModal(ModalMap['9'], {
address: currentNetwork.ssid,
});
}else{
openModal(ModalMap['8'], {
address: currentNetwork.ssid,
});
}
};
useEffect(() => {
const perc = Number(network.signal);
switch (true) {
case perc >= -50: setSignal(5); break;
case perc >= -60: setSignal(4); break;
case perc >= -70: setSignal(3); break;
case perc >= -80: setSignal(2); break;
default: setSignal(1); break;
}
}, [network.signal])
return (
<Modal>
<CloseBtn className={'close-btn'} onClick={onClose}>
<IconComponent name={buttonNames[12]} width="30" height="30" />
</CloseBtn>
<ModalTitle>
<h4>{network.ssid}</h4>
<div className={'functionalIcons'}>
<span onClick={openModalNote} style={{cursor: "pointer"}}>
<IconComponent name={buttonNames[16]} width="30" height="30" />
</span>
<Status>
<SignalSVG signal={signal} size={'36px'} color={theme.accent} />
</Status>
</div>
</ModalTitle>
<ModalContent>
<ModalRow>
<h6>BSSID:</h6>
<p>{network.bssid}</p>
</ModalRow>
<ModalRow>
<h6>Производитель:</h6>
<p>{network.vendor}</p>
</ModalRow>
<ModalRow>
<h6>Сигнал:</h6>
<p>{network.signal} dBm</p>
</ModalRow>
<ModalRow>
<h6>Канал:</h6>
<p>{network.channel}</p>
</ModalRow>
<ModalRow>
<h6>Шифрование:</h6>
<p>{network.encryption}</p>
</ModalRow>
<ModalRow>
<h6>Частота:</h6>
<p>{network.frequency}</p>
</ModalRow>
<ModalRow>
<h6>Скрытая сеть:</h6>
<p>{network.hidden ? 'Да' : 'Нет'}</p>
</ModalRow>
<ModalRow>
<h6>Заметка:</h6>
<p>{currentNetwork.note ? currentNetwork.note : '-'}</p>
</ModalRow>
<ModalConnect onClick={openModalNetwork}>{isConnected ? 'Отключиться' : 'Подключиться'}</ModalConnect>
</ModalContent>
</Modal>
);
};
export default ModalWifiScaner;

View file

@ -0,0 +1,79 @@
import React from 'react';
import styled from "styled-components";
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
note: string;
}
//Точка вайфай
const Dot = styled.div<{ top: string; left: string }>`
position: absolute;
top: ${props => props.top};
left: ${props => props.left};
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
transform: translate(-50%, -50%);
user-select: none;
transition: .4s;
text-wrap: nowrap;
&:hover {
transform: translate(-50%, -50%) scale(1.1);
}
`;
const WifiDot = ({ network, onSelect }: { network: WifiNetworkScanner, onSelect: (n: WifiNetworkScanner) => void }) => {
const getPositionInRing = (bssid: string, ringSize: number) => {
const hash = bssid.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const randomAngle = (hash % 360) * (Math.PI / 180);
const innerRadius = ringSize * 0.4;
const outerRadius = ringSize * 0.5;
const randomValue = (hash % 1000) / 1000;
const randomRadius = Math.sqrt(innerRadius * innerRadius + randomValue * (outerRadius * outerRadius - innerRadius * innerRadius));
return {
x: Math.cos(randomAngle) * randomRadius,
y: Math.sin(randomAngle) * randomRadius
};
};
const getPosition = (signal: number, bssid: string) => {
let strength: 1 | 2 | 3;
if (signal >= -50) {
strength = 3;
} else if (signal >= -70) {
strength = 2;
} else if (signal >= -90) {
strength = 1;
} else {
strength = 1;
}
const ringSizes = { 1: 550, 2: 400, 3: 250 };
const ringSize = ringSizes[strength];
const offset = getPositionInRing(bssid, ringSize);
return {
top: `calc(50% - ${offset.y}px)`,
left: `calc(50% - ${offset.x}px)`
};
};
const position = getPosition(network.signal, network.bssid);
return (
<Dot top={position.top} left={position.left} onClick={() => onSelect(network)}>
{network.ssid}
</Dot>
);
};
export default WifiDot;

View file

@ -50,10 +50,19 @@ export const ToolBar = () => {
{toolBarMenu && toolBarMenu.map((menu) => ( {toolBarMenu && toolBarMenu.map((menu) => (
<ToolBarButton menuId={menu.id} key={menu.id} /> <ToolBarButton menuId={menu.id} key={menu.id} />
))} ))}
<Button>
<IconContainer>
<IconComponent name={buttonNames[14]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[5])} />
</IconContainer>
</Button>
<Button>
<IconContainer>
<IconComponent name={buttonNames[15]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[6])} />
</IconContainer>
</Button>
</ButtonContainer> </ButtonContainer>
<ButtonContainer> <ButtonContainer>
<Button> <Button>
<IconContainer> <IconContainer>
<IconComponent name={isAdmin ? buttonNames[10] : buttonNames[13]} width="30" height="30" onClick={() => openModal(ModalMap['5'])} /> <IconComponent name={isAdmin ? buttonNames[10] : buttonNames[13]} width="30" height="30" onClick={() => openModal(ModalMap['5'])} />

View file

@ -18,7 +18,7 @@ const Container = styled.div`
align-items: center; align-items: center;
gap: 15px; gap: 15px;
padding: 20px; padding: 20px;
width: 210px;
p{ p{
margin: 0; margin: 0;
} }
@ -196,12 +196,12 @@ export const DevicesBar = () => {
<DeviceBlock> <DeviceBlock>
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}> <DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
<p>Текущее устройство</p> <p>Корневой адрес</p>
</DeviceControl> </DeviceControl>
{currentDevice.hostname == '' ? ( {currentDevice.hostname == '' ? (
<Text className="paddingP">Адрес: Загрузка<AnimDot /></Text> <Text className="paddingP">Название: Загрузка<AnimDot /></Text>
) : ( ) : (
<p className="paddingP">Адрес: {currentDevice.hostname}</p> <p className="paddingP">Название: {currentDevice.hostname}</p>
)} )}
</DeviceBlock> </DeviceBlock>

View file

@ -3,11 +3,15 @@ import { useToolBar } from "../../../../hooks/useToolBar"
import { Space } from "../../../../const/menus/sideBar/components" import { Space } from "../../../../const/menus/sideBar/components"
import styled from "styled-components" import styled from "styled-components"
const Container = styled.div` const Container = styled.div<{isOpen: boolean}>`
background-color: ${(props) => props.theme.secondary}; background-color: ${(props) => props.theme.secondary};
width: 250px; width: 250px;
font-size: 14px; font-size: 14px;
flex: 3; flex: 3;
overflow: hidden;
max-width: ${({isOpen})=>(isOpen ? '250px' : '0')};
transition: max-width .4s ease;
` `
export const ToolSpace = () => { export const ToolSpace = () => {
@ -15,12 +19,12 @@ export const ToolSpace = () => {
const name = toolBar?.current || '' const name = toolBar?.current || ''
const { isActive, isOpen } = useToolBar(name) const { isActive, isOpen } = useToolBar(name)
if (!isActive || !isOpen) { if (!isActive) {
return null return null
} }
return ( return (
<Container> <Container isOpen={isOpen}>
<Space name={name} /> <Space name={name} />
</Container> </Container>
) )

View file

@ -9,6 +9,8 @@ import { workSpaceNames } from "../../const/workspace/names";
import { Header } from "./modules/appModules/header"; import { Header } from "./modules/appModules/header";
import { ReactFlowProvider } from "@xyflow/react"; import { ReactFlowProvider } from "@xyflow/react";
import About from "./modules/appModules/about"; import About from "./modules/appModules/about";
import UserScanner from "./modules/appModules/usersScanner/userScanner";
import {WifiScanner} from "./modules/appModules/wifiScanner/WifiScanner";
const Section = styled.div` const Section = styled.div`
display: flex; display: flex;
@ -75,6 +77,12 @@ export default function WorkSpace() {
from={"helloWindow"} from={"helloWindow"}
/> />
)} )}
{workModule === workSpaceNames[5] && (
<UserScanner />
)}
{workModule === workSpaceNames[6] && (
<WifiScanner />
)}
{workModule === workSpaceNames[3] && {workModule === workSpaceNames[3] &&
<WorkSection> <WorkSection>
<Header /> <Header />

View file

@ -12,10 +12,13 @@ import arrowRight from '@iconify/icons-ic/baseline-keyboard-arrow-right';
import edit from '@iconify/icons-ic/baseline-edit'; import edit from '@iconify/icons-ic/baseline-edit';
import checkCircle from '@iconify/icons-ic/round-check-circle'; import checkCircle from '@iconify/icons-ic/round-check-circle';
import cancel from '@iconify/icons-ic/baseline-cancel'; import cancel from '@iconify/icons-ic/baseline-cancel';
import userSearch from '@iconify/icons-ic/baseline-person-search';
import admin from '@iconify/icons-ic/baseline-admin-panel-settings'; import admin from '@iconify/icons-ic/baseline-admin-panel-settings';
import noAdmin from '@iconify/icons-ic/baseline-remove-moderator' import noAdmin from '@iconify/icons-ic/baseline-remove-moderator'
import refresh from '@iconify/icons-ic/baseline-refresh'; import refresh from '@iconify/icons-ic/baseline-refresh';
import close from '@iconify/icons-ic/round-close'; import close from '@iconify/icons-ic/round-close';
import wifiFind from '@iconify/icons-ic/baseline-wifi-find';
import noteEdit from '@iconify/icons-ph/note-pencil';
export const ICONS = { export const ICONS = {
[buttonNames[1]]: exitToApp, [buttonNames[1]]: exitToApp,
@ -30,8 +33,10 @@ export const ICONS = {
[buttonNames[10]]: admin, [buttonNames[10]]: admin,
[buttonNames[11]]: refresh, [buttonNames[11]]: refresh,
[buttonNames[12]]: close, [buttonNames[12]]: close,
[buttonNames[13]]: noAdmin [buttonNames[13]]: noAdmin,
[buttonNames[14]]: userSearch,
[buttonNames[15]]: wifiFind,
[buttonNames[16]]: noteEdit,
} as const; } as const;
type IconName = keyof typeof ICONS; type IconName = keyof typeof ICONS;

View file

@ -13,5 +13,8 @@ export const buttonNames: names ={
10: 'admin', 10: 'admin',
11: 'reset', 11: 'reset',
12: 'close', 12: 'close',
13: 'noAdmin' 13: 'noAdmin',
14: 'userSearch',
15: 'wifiFind',
16: 'noteEdit'
} }

View file

@ -1,5 +1,7 @@
const baseTheme = { const baseTheme = {
accent: '#007ACC', accent: '#007ACC',
successColor: "#4CAf50",
errorColor: "#F44336",
accentHover: '#4A90E2', accentHover: '#4A90E2',
} }
@ -9,6 +11,10 @@ export const light = {
secondary: '#EDEDED', secondary: '#EDEDED',
background: '#E8E8E8', background: '#E8E8E8',
textColor: '#000000', textColor: '#000000',
borderColor: '#404040',
scannerColor: '#007ACC',
offlineColor: '#ababab',
hoverBtn: '#d6d6d6',
}; };
export const dark = { export const dark = {
@ -17,4 +23,8 @@ export const dark = {
secondary: '#252525', secondary: '#252525',
background: '#3A3A3A', background: '#3A3A3A',
textColor: '#FFFFFF', textColor: '#FFFFFF',
borderColor: '#b6b6b6',
scannerColor: '#0E9C9D',
offlineColor: '#656565',
hoverBtn: '#4a4a4a',
}; };

View file

@ -4,5 +4,7 @@ export const workSpaceNames: names ={
1: 'helloWindow', 1: 'helloWindow',
2: 'aboutFromWork', 2: 'aboutFromWork',
3: 'networkInterfaces', 3: 'networkInterfaces',
4: 'aboutFromHello' 4: 'aboutFromHello',
5: 'userScanner',
6: 'wifiScanner',
} }

View file

@ -9,7 +9,7 @@ interface IpAddressState {
export const useIpStore = create<IpAddressState>()( export const useIpStore = create<IpAddressState>()(
persist( persist(
(set)=>({ (set)=>({
ipAddress: 'localhost', ipAddress: '192:168.1.147',
setIpAddress: (newIp: string) => set({ ipAddress: newIp }), setIpAddress: (newIp: string) => set({ ipAddress: newIp }),
}), }),
{ {

View file

@ -0,0 +1,51 @@
import { create } from 'zustand';
export interface DeviceUser {
ip: string;
mac: string;
hostname: string;
vendor: string;
open_ports: string[];
status: string;
last_seen: string;
note: string;
}
export interface WifiNetworkScanner {
ssid: string;
bssid: string;
channel: number;
signal: number;
encryption: string;
frequency: string;
vendor: string;
hidden: boolean;
note: string;
}
interface NetworkStore {
devices: DeviceUser[];
setDevices: (devices: DeviceUser[]) => void;
addDevice: (device: DeviceUser) => void;
wifiNetworks: WifiNetworkScanner[];
setWifiNetworks: (networks: WifiNetworkScanner[]) => void;
addWifiNetwork: (network: WifiNetworkScanner) => void;
clearAll: () => void;
}
const useNetworkStore = create<NetworkStore>((set) => ({
devices: [],
setDevices: (devices) => set({ devices }),
addDevice: (device) =>
set((state) => ({ devices: [...state.devices, device] })),
wifiNetworks: [],
setWifiNetworks: (networks) => set({ wifiNetworks: networks }),
addWifiNetwork: (network) =>
set((state) => ({ wifiNetworks: [...state.wifiNetworks, network] })),
clearAll: () => set({ devices: [], wifiNetworks: [] })
}));
export default useNetworkStore;

View file

@ -1,6 +1,5 @@
import { useIpStore } from "../store"; import { useIpStore } from "../store";
import { useAdminStore } from "../store/adminStore"; import { useAdminStore } from "../store/adminStore";
import { useEffect } from "react";
import { NetworkService } from "../api/request"; import { NetworkService } from "../api/request";
export const useCheckAdmin = () => { export const useCheckAdmin = () => {

56
FRONT/README.md Normal file
View file

@ -0,0 +1,56 @@
## Клиент приложения ГоNets
### Общие сведения
ГоNets - приложение для работы с сетевыми ресурсами в удобном и понятном графическом виде. Данное приложение разделено на серверную и клиентскую часть. Сервер сканирует, обрабатывает и преобразует информацию по сетям в простой и понятный JSON формат, который в свою очередь приходит на клиент, где данные проходят ряд обработок и формируются в графическое отображение.
Клиент приложения ГоNets написан на языке Typescript с использованием фреймворка React. Для более понятного отображения информации была выбрана библиотека ReatFlow, позволяющая легко и просто выстраивать различные графы, схемы, блоки и их связи. Так же, для того, чтобы приложение запускалось как десктопное был выбран метод обёртки собранной (build) версии в облочку Electron.
### Зависимости клиенсткой части
Далее будет представлен список необходимых npm зависимостей для разработки приложения. Уточню, что необходимы они исключительно во время разработки и сборки, собранная версия работает спокойно без них.
Зависимости при разработки:
```
"dependencies": {
"@iconify/icons-ic": "^1.2.13",
"@iconify/react": "^5.2.0",
"@reduxjs/toolkit": "^2.6.0",
"@xyflow/react": "^12.3.5",
"axios": "^1.7.9",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-flow-renderer": "^10.3.17",
"react-redux": "^9.2.0",
"react-router-dom": "^7.0.2",
"styled-components": "^6.1.13",
"zustand": "^5.0.3"
},
"devDependencies": {
"@iconify-json/ic": "^1.2.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-react": "^4.4.1",
"electron": "^34.0.2",
"electron-packager": "^17.1.2",
"esbuild-wasm": "^0.17.12",
"typescript": "^4.9.5",
"vite": "^6.3.4"
}
```
### Запуск и сборка
Для того, чтобы работать с исходным кодом, необходимо склонировать проект с ветки frontend, перейти в директорию `FRONT/DEV/` и выполнить установку зависимостей с помощью `npm install`, возможно потребуется сделать после этого `npm audit --fix` Весь исходный код, находится непосредственно в директории `DEV`, собранная же версия в директории `BUILD`.
Для сборки приложения и обёртки его в электрон необходимо выполнить команду `npm run package`, далее перейти в созданную после команды директорию `dist`, от туда в `gonets-for-linux-64x/resources/app`, здесь нужно выполнить `npm run build`, в появившейся директории `dist` отредактировать файл `index.html`, таким образом, что везде где стоит `/assets` нужно прописать `./assets`. Проверить запуск приложения, можно прописав в терминале `./gonets` из директории `FRONT/DEV/dist/gonets-for-linux-64x`, где и лежит исполняемвый файл.
После удачного билда можно переместить всё содержимое `FRONT/DEV/dist/gonets-for-linux-64x/` в `/FRONT/BUILD`
Дополнительно отмечу, что входной точкой/файлом настройки самого электрона является файл `main.js`

View file

@ -1,3 +1,3 @@
# GoNets # GoNets
Общий репозиторий для GoNets начиная с версии 0.0.2 Общий репозиторий для GoNets начиная с версии 0.0.1

1
SRV/.gitignore vendored
View file

@ -6,3 +6,4 @@ __pycache__
.wheels .wheels
.vendor .vendor
.python3-deb .python3-deb
storage/devices/

View file

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

View file

@ -26,7 +26,8 @@ async def register(user: UserCreate):
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось зарегистрировать пользователя") raise HTTPException(status_code=500,
detail="Не удалось зарегистрировать пользователя")
@router.post("/login", response_model=Token) @router.post("/login", response_model=Token)
@ -36,13 +37,16 @@ async def login(user: UserLogin, request: Request):
client_id = request.client.host client_id = request.client.host
logger.debug(f"Попытка авторизации для client_id {client_id}") logger.debug(f"Попытка авторизации для client_id {client_id}")
result = auth_service.login(user, client_id) result = auth_service.login(user, client_id)
logger.info(f"Успешная авторизация для client_id {client_id}, session_id: {result['session_id']}") logger.info(
f"Успешная авторизация для client_id {client_id}, session_id: {result['session_id']}"
)
return result return result
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось авторизовать пользователя") raise HTTPException(status_code=500,
detail="Не удалось авторизовать пользователя")
@router.get("/me") @router.get("/me")
@ -55,13 +59,15 @@ async def get_current_user(request: Request):
return { return {
"user": result["user"], "user": result["user"],
"access_token": result["access_token"], "access_token": result["access_token"],
"token_type": "bearer" "token_type": "bearer",
} }
except HTTPException as e: except HTTPException as e:
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}") logger.error(
raise HTTPException(status_code=500, detail="Не удалось получить данные пользователя") f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500,
detail="Не удалось получить данные пользователя")
@router.get("/logout") @router.get("/logout")
@ -75,7 +81,8 @@ async def logout(request: Request):
raise e raise e
except Exception as e: except Exception as e:
logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}") logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось выйти из системы") raise HTTPException(status_code=500,
detail="Не удалось выйти из системы")
@router.get("/check_json") @router.get("/check_json")
@ -90,25 +97,34 @@ async def check_users_file():
return { return {
"exists": False, "exists": False,
"has_users": False, "has_users": False,
"message": "Файл users.json не существует" "message": "Файл users.json не существует",
} }
data = storage._read_data() data = storage._read_data()
has_users = len(data.get("users", [])) > 0 has_users = len(data.get("users", [])) > 0
if has_users: if has_users:
logger.info(f"Файл {file_path} существует и содержит пользователей") logger.info(
f"Файл {file_path} существует и содержит пользователей")
return { return {
"exists": True, "exists": True,
"has_users": True, "has_users": True,
"message": "Файл users.json существует и содержит пользователей" "message":
"Файл users.json существует и содержит пользователей",
} }
else: else:
logger.info(f"Файл {file_path} существует, но не содержит пользователей") logger.info(
f"Файл {file_path} существует, но не содержит пользователей")
return { return {
"exists": True, "exists":
"has_users": False, True,
"message": "Файл users.json существует, но не содержит пользователей" "has_users":
False,
"message":
"Файл users.json существует, но не содержит пользователей",
} }
except Exception as e: except Exception as e:
logger.error(f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}") logger.error(
raise HTTPException(status_code=500, detail="Ошибка при проверке файла") f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}"
)
raise HTTPException(status_code=500,
detail="Ошибка при проверке файла")

View file

@ -31,7 +31,9 @@ class SuccessResponse(BaseModel):
@router.get( @router.get(
"/zones", "/zones",
response_model=List[str], response_model=List[str],
responses={500: {"model": ErrorResponse}}, responses={500: {
"model": ErrorResponse
}},
summary="Get all available zones", summary="Get all available zones",
) )
async def get_zones() -> List[str]: async def get_zones() -> List[str]:
@ -53,7 +55,9 @@ async def get_zones() -> List[str]:
@router.get( @router.get(
"/zones/default", "/zones/default",
response_model=str, response_model=str,
responses={500: {"model": ErrorResponse}}, responses={500: {
"model": ErrorResponse
}},
summary="Get default zone", summary="Get default zone",
) )
async def get_default_zone() -> str: async def get_default_zone() -> str:
@ -75,7 +79,14 @@ async def get_default_zone() -> str:
@router.put( @router.put(
"/zones/default/{zone}", "/zones/default/{zone}",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Set default zone", summary="Set default zone",
) )
async def set_default_zone( async def set_default_zone(
@ -94,9 +105,8 @@ async def set_default_zone(
firewalld.set_default_zone(zone) firewalld.set_default_zone(zone)
return SuccessResponse(message=f"Default zone set to {zone}") return SuccessResponse(message=f"Default zone set to {zone}")
except ValueError: except ValueError:
raise HTTPException( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found" detail=f"Zone '{zone}' not found")
)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -107,10 +117,18 @@ async def set_default_zone(
@router.get( @router.get(
"/zones/{zone}", "/zones/{zone}",
response_model=FirewallZone, response_model=FirewallZone,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Get zone configuration", summary="Get zone configuration",
) )
async def get_zone(zone: str = Path(..., description="Zone name")) -> FirewallZone: async def get_zone(zone: str = Path(...,
description="Zone name")) -> FirewallZone:
""" """
Get detailed configuration for a specific zone. Get detailed configuration for a specific zone.
@ -122,16 +140,13 @@ async def get_zone(zone: str = Path(..., description="Zone name")) -> FirewallZo
""" """
try: try:
zone_info = firewalld.get_zone_settings(zone) zone_info = firewalld.get_zone_settings(zone)
return FirewallZone( return FirewallZone(name=zone,
name=zone,
target=FirewallTarget.DEFAULT, target=FirewallTarget.DEFAULT,
interfaces=[], interfaces=[],
**zone_info **zone_info)
)
except ValueError: except ValueError:
raise HTTPException( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found" detail=f"Zone '{zone}' not found")
)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -142,10 +157,13 @@ async def get_zone(zone: str = Path(..., description="Zone name")) -> FirewallZo
@router.get( @router.get(
"/zones/{zone}/services", "/zones/{zone}/services",
response_model=List[str], response_model=List[str],
responses={500: {"model": ErrorResponse}}, responses={500: {
"model": ErrorResponse
}},
summary="Get services in zone", summary="Get services in zone",
) )
async def get_services(zone: str = Path(..., description="Zone name")) -> List[str]: async def get_services(zone: str = Path(...,
description="Zone name")) -> List[str]:
""" """
Get all services in a zone. Get all services in a zone.
@ -167,7 +185,14 @@ async def get_services(zone: str = Path(..., description="Zone name")) -> List[s
@router.post( @router.post(
"/zones/{zone}/services", "/zones/{zone}/services",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Add service to zone", summary="Add service to zone",
) )
async def add_service( async def add_service(
@ -186,11 +211,11 @@ async def add_service(
""" """
try: try:
firewalld.add_service(zone, rule.name, rule.timeout) firewalld.add_service(zone, rule.name, rule.timeout)
return SuccessResponse(message=f"Service {rule.name} added to zone {zone}") return SuccessResponse(
message=f"Service {rule.name} added to zone {zone}")
except ValueError: except ValueError:
raise HTTPException( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found" detail=f"Zone '{zone}' not found")
)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -201,7 +226,14 @@ async def add_service(
@router.delete( @router.delete(
"/zones/{zone}/services/{service}", "/zones/{zone}/services/{service}",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Remove service from zone", summary="Remove service from zone",
) )
async def remove_service( async def remove_service(
@ -220,7 +252,8 @@ async def remove_service(
""" """
try: try:
firewalld.remove_service(zone, service) firewalld.remove_service(zone, service)
return SuccessResponse(message=f"Service {service} removed from zone {zone}") return SuccessResponse(
message=f"Service {service} removed from zone {zone}")
except ValueError: except ValueError:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -236,10 +269,13 @@ async def remove_service(
@router.get( @router.get(
"/zones/{zone}/ports", "/zones/{zone}/ports",
response_model=List[str], response_model=List[str],
responses={500: {"model": ErrorResponse}}, responses={500: {
"model": ErrorResponse
}},
summary="Get ports in zone", summary="Get ports in zone",
) )
async def get_ports(zone: str = Path(..., description="Zone name")) -> List[str]: async def get_ports(zone: str = Path(...,
description="Zone name")) -> List[str]:
""" """
Get all ports in a zone. Get all ports in a zone.
@ -262,9 +298,15 @@ async def get_ports(zone: str = Path(..., description="Zone name")) -> List[str]
"/zones/{zone}/ports", "/zones/{zone}/ports",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={ responses={
400: {"model": ErrorResponse}, 400: {
404: {"model": ErrorResponse}, "model": ErrorResponse
500: {"model": ErrorResponse}, },
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
},
}, },
summary="Add port to zone", summary="Add port to zone",
) )
@ -285,10 +327,11 @@ async def add_port(
try: try:
firewalld.add_port(zone, rule.port, rule.protocol.value, rule.timeout) firewalld.add_port(zone, rule.port, rule.protocol.value, rule.timeout)
return SuccessResponse( return SuccessResponse(
message=f"Port {rule.port}/{rule.protocol.value} added to zone {zone}" message=
) f"Port {rule.port}/{rule.protocol.value} added to zone {zone}")
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e))
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -299,7 +342,14 @@ async def add_port(
@router.delete( @router.delete(
"/zones/{zone}/ports", "/zones/{zone}/ports",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Remove port from zone", summary="Remove port from zone",
) )
async def remove_port( async def remove_port(
@ -319,12 +369,11 @@ async def remove_port(
try: try:
firewalld.remove_port(zone, rule.port, rule.protocol.value) firewalld.remove_port(zone, rule.port, rule.protocol.value)
return SuccessResponse( return SuccessResponse(
message=f"Port {rule.port}/{rule.protocol.value} removed from zone {zone}" message=
) f"Port {rule.port}/{rule.protocol.value} removed from zone {zone}")
except ValueError: except ValueError:
raise HTTPException( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found" detail=f"Zone '{zone}' not found")
)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -335,10 +384,13 @@ async def remove_port(
@router.get( @router.get(
"/zones/{zone}/sources", "/zones/{zone}/sources",
response_model=List[str], response_model=List[str],
responses={500: {"model": ErrorResponse}}, responses={500: {
"model": ErrorResponse
}},
summary="Get sources in zone", summary="Get sources in zone",
) )
async def get_sources(zone: str = Path(..., description="Zone name")) -> List[str]: async def get_sources(zone: str = Path(...,
description="Zone name")) -> List[str]:
""" """
Get all sources in a zone. Get all sources in a zone.
@ -360,7 +412,14 @@ async def get_sources(zone: str = Path(..., description="Zone name")) -> List[st
@router.post( @router.post(
"/zones/{zone}/sources", "/zones/{zone}/sources",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Add source to zone", summary="Add source to zone",
) )
async def add_source( async def add_source(
@ -379,11 +438,11 @@ async def add_source(
""" """
try: try:
firewalld.add_source(zone, rule.address) firewalld.add_source(zone, rule.address)
return SuccessResponse(message=f"Source {rule.address} added to zone {zone}") return SuccessResponse(
message=f"Source {rule.address} added to zone {zone}")
except ValueError: except ValueError:
raise HTTPException( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found" detail=f"Zone '{zone}' not found")
)
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -394,7 +453,14 @@ async def add_source(
@router.delete( @router.delete(
"/zones/{zone}/sources/{source}", "/zones/{zone}/sources/{source}",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}}, responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Remove source from zone", summary="Remove source from zone",
) )
async def remove_source( async def remove_source(
@ -413,7 +479,8 @@ async def remove_source(
""" """
try: try:
firewalld.remove_source(zone, source) firewalld.remove_source(zone, source)
return SuccessResponse(message=f"Source {source} removed from zone {zone}") return SuccessResponse(
message=f"Source {source} removed from zone {zone}")
except ValueError: except ValueError:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
@ -429,7 +496,9 @@ async def remove_source(
@router.post( @router.post(
"/reload", "/reload",
response_model=SuccessResponse, response_model=SuccessResponse,
responses={500: {"model": ErrorResponse}}, responses={500: {
"model": ErrorResponse
}},
summary="Reload firewall rules", summary="Reload firewall rules",
) )
async def reload_firewall() -> SuccessResponse: async def reload_firewall() -> SuccessResponse:

View file

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

View file

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

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

@ -0,0 +1,159 @@
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):
"""Сканирует локальную подсеть и возвращает данные об устройствах"""
try:
subnet_service = SubnetService()
result = subnet_service.scan_subnet(subnet, ports)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка сканирования: {str(e)}")
@router.get("/wifi", response_model=WifiScanResult)
def scan_wifi(timeout: int = 30):
"""Сканирует Wi-Fi точки доступа и возвращает данные о них"""
try:
wifi_service = WifiService()
result = wifi_service.scan_wifi(timeout=timeout)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка сканирования Wi-Fi: {str(e)}")
@router.post("/devices/{mac}/note")
def add_note(mac: str, request: NoteRequest):
"""Добавляет заметку для устройства по MAC-адресу"""
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-адресу"""
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-адресу"""
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"""
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"""
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"""
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 и паролю."""
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."""
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."""
wifi_service = WifiConnectionService()
success, message = wifi_service.get_status()
if not success:
raise HTTPException(status_code=400, detail=message)
return {"status": "connected", "ssid": message}

View file

@ -1,13 +1,17 @@
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from services.ufw_service import UFWService
from models.ufw import UFWRulesResponse, UFWStatus, UFWRule
from typing import List from typing import List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from models.ufw import UFWRulesResponse, UFWStatus, UFWRule
from services.ufw_service import UFWService
router = APIRouter() router = APIRouter()
class RuleNumbers(BaseModel): class RuleNumbers(BaseModel):
numbers: List[int] = Field(..., description="Список номеров правил для удаления") numbers: List[int] = Field(
..., description="Список номеров правил для удаления")
@router.get("/rules", response_model=UFWRulesResponse) @router.get("/rules", response_model=UFWRulesResponse)
async def get_ufw_rules(): async def get_ufw_rules():
@ -20,6 +24,7 @@ async def get_ufw_rules():
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}") raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.get("/status", response_model=UFWStatus) @router.get("/status", response_model=UFWStatus)
async def get_ufw_status(): async def get_ufw_status():
""" """
@ -31,6 +36,7 @@ async def get_ufw_status():
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}") raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/enable") @router.post("/enable")
async def enable_ufw(): async def enable_ufw():
""" """
@ -42,10 +48,12 @@ async def enable_ufw():
if success: if success:
return {"status": "success", "message": "UFW успешно включен"} return {"status": "success", "message": "UFW успешно включен"}
else: else:
raise HTTPException(status_code=500, detail="Не удалось включить UFW") raise HTTPException(status_code=500,
detail="Не удалось включить UFW")
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}") raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/disable") @router.post("/disable")
async def disable_ufw(): async def disable_ufw():
""" """
@ -57,10 +65,12 @@ async def disable_ufw():
if success: if success:
return {"status": "success", "message": "UFW успешно выключен"} return {"status": "success", "message": "UFW успешно выключен"}
else: else:
raise HTTPException(status_code=500, detail="Не удалось выключить UFW") raise HTTPException(status_code=500,
detail="Не удалось выключить UFW")
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}") raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/rules/in") @router.post("/rules/in")
async def add_ufw_in_rules(rules: List[UFWRule]): async def add_ufw_in_rules(rules: List[UFWRule]):
""" """
@ -73,23 +83,29 @@ async def add_ufw_in_rules(rules: List[UFWRule]):
if not status.enabled: if not status.enabled:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="UFW отключен. Включите UFW перед добавлением правил." detail="UFW отключен. Включите UFW перед добавлением правил.",
) )
result = ufw_service.add_rules(rules) result = ufw_service.add_rules(rules)
if result["success"]: if result["success"]:
successful_rules = len([r for r in result["results"] if r["success"]]) successful_rules = len(
[r for r in result["results"] if r["success"]])
total_rules = len(result["results"]) total_rules = len(result["results"])
return { return {
"status": "success", "status": "success",
"message": f"Добавлено {successful_rules} из {total_rules} правил", "message":
"details": result f"Добавлено {successful_rules} из {total_rules} правил",
"details": result,
} }
else: else:
raise HTTPException(status_code=500, detail=result.get("error", "Не удалось добавить правила")) raise HTTPException(
status_code=500,
detail=result.get("error", "Не удалось добавить правила"),
)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}") raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/rules/out") @router.post("/rules/out")
async def add_ufw_out_rules(rules: List[UFWRule]): async def add_ufw_out_rules(rules: List[UFWRule]):
""" """
@ -102,17 +118,23 @@ async def add_ufw_out_rules(rules: List[UFWRule]):
if not status.enabled: if not status.enabled:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="UFW отключен. Включите UFW перед добавлением правил." detail="UFW отключен. Включите UFW перед добавлением правил.",
) )
success = ufw_service.add_ufw_out_rules(rules) success = ufw_service.add_ufw_out_rules(rules)
if success: if success:
return {"status": "success", "message": "Исходящие правила UFW успешно добавлены"} return {
"status": "success",
"message": "Исходящие правила UFW успешно добавлены",
}
else: else:
raise HTTPException(status_code=500, detail="Не удалось добавить исходящие правила UFW") raise HTTPException(
status_code=500,
detail="Не удалось добавить исходящие правила UFW")
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}") raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.delete("/rules") @router.delete("/rules")
async def delete_ufw_rules(rule_numbers: RuleNumbers): async def delete_ufw_rules(rule_numbers: RuleNumbers):
""" """
@ -124,8 +146,9 @@ async def delete_ufw_rules(rule_numbers: RuleNumbers):
if result["success"]: if result["success"]:
return { return {
"status": "success", "status": "success",
"message": f"Удалено {result['successful']} из {result['total']} правил", "message":
"details": result f"Удалено {result['successful']} из {result['total']} правил",
"details": result,
} }
else: else:
raise HTTPException(status_code=500, detail=result["error"]) raise HTTPException(status_code=500, detail=result["error"])

View file

@ -12,10 +12,12 @@ router = APIRouter()
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class WebSocketManager: class WebSocketManager:
""" """
Класс для управления подключениями к WebSocket-серверу Класс для управления подключениями к WebSocket-серверу
""" """
def __init__(self): def __init__(self):
"""Инициализация менеджера подключений к WebSocket-серверу""" """Инициализация менеджера подключений к WebSocket-серверу"""
self.active_connections: Set[WebSocket] = set() self.active_connections: Set[WebSocket] = set()
@ -41,8 +43,10 @@ class WebSocketManager:
logger.error(f"Сообщение об ошибке трансляции: {e}") logger.error(f"Сообщение об ошибке трансляции: {e}")
self.disconnect(connection) self.disconnect(connection)
manager = WebSocketManager() manager = WebSocketManager()
@router.websocket("/api/v1") @router.websocket("/api/v1")
async def websocket_endpoint(websocket: WebSocket): async def websocket_endpoint(websocket: WebSocket):
"""Обработчик WebSocket-соединения""" """Обработчик WebSocket-соединения"""
@ -52,7 +56,8 @@ async def websocket_endpoint(websocket: WebSocket):
data = await websocket.receive_json() data = await websocket.receive_json()
logger.info(f"Полученные данные: {data}") logger.info(f"Полученные данные: {data}")
if all(key in data for key in ["container_id", "container_x", "container_y"]): if all(key in data
for key in ["container_id", "container_x", "container_y"]):
await update_container_position(data) await update_container_position(data)
network_data = await get_network_data() network_data = await get_network_data()
@ -69,18 +74,22 @@ async def websocket_endpoint(websocket: WebSocket):
if websocket.client_state != WebSocketState.DISCONNECTED: if websocket.client_state != WebSocketState.DISCONNECTED:
await websocket.close() await websocket.close()
@router.on_event("startup") @router.on_event("startup")
async def startup_event(): async def startup_event():
"""Запуск таска для трансляции сетевых данных""" """Запуск таска для трансляции сетевых данных"""
asyncio.create_task(broadcast_data()) asyncio.create_task(broadcast_data())
async def broadcast_data(): async def broadcast_data():
"""Таск для трансляции сетевых данных""" """Таск для трансляции сетевых данных"""
while True: while True:
try: try:
network_data = await get_network_data() network_data = await get_network_data()
if "activation" not in network_data: if "activation" not in network_data:
logger.error("Ошибка: Ключ 'activation' отсутствует в полученных данных сети") logger.error(
"Ошибка: Ключ 'activation' отсутствует в полученных данных сети"
)
await asyncio.sleep(2) await asyncio.sleep(2)
continue continue

View file

@ -6,6 +6,7 @@ class CORSMiddlewareConfig:
""" """
Класс для настройки CORS политики в FastAPI Класс для настройки CORS политики в FastAPI
""" """
def __init__(self, app: FastAPI) -> None: def __init__(self, app: FastAPI) -> None:
"""Инициализируем приложение FastAPI и список допустимых CORS-оригинов""" """Инициализируем приложение FastAPI и список допустимых CORS-оригинов"""
self.app = app self.app = app

View file

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

102
SRV/config/ports.py Normal file
View 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()

View file

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

View file

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

View file

View file

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

View file

@ -19,4 +19,4 @@ network_service = NetworkService()
if __name__ == "__main__": 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, 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) #uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=True)

View file

@ -1,22 +1,61 @@
# models.py
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional from typing import Optional
class UserCreate(BaseModel): class UserCreate(BaseModel):
"""
Модель созданного пользователя
Атрибуты:
username (str): Имя пользователя
password (str): Пароль пользователя
is_admin (bool): Административный доступ
"""
username: str username: str
password: str password: str
is_admin: bool = False is_admin: bool = False
class UserLogin(BaseModel): class UserLogin(BaseModel):
"""
Модель авторизации пользователя
Атрибуты:
username (str): Имя пользователя
password (str): Пароль пользователя
"""
username: str username: str
password: str password: str
class UserResponse(BaseModel): class UserResponse(BaseModel):
"""
Модель проверки наличия пользователя
Атрибуты:
id (int): Идентификатор
username (str): Имя пользователя
is_admin (bool): Административный доступ
"""
id: int id: int
username: str username: str
is_admin: bool is_admin: bool
class Token(BaseModel): class Token(BaseModel):
"""
Токен пользователя
Атрибуты:
access_token (str): Токен пользователя (захэширован)
token_type (str): Тип токена
session_id (Optional[str]): Идентификатор сессии
user (Optional[UserResponse]): Пользователь на основе проверки наличия пользователя
"""
access_token: str access_token: str
token_type: str token_type: str
session_id: Optional[str] = None session_id: Optional[str] = None

View file

@ -25,42 +25,35 @@ class FirewallZone(BaseModel):
"""Represents a firewalld zone configuration""" """Represents a firewalld zone configuration"""
name: Annotated[str, StringConstraints(min_length=1)] = Field( name: Annotated[str, StringConstraints(min_length=1)] = Field(
..., description="Name of the zone" ..., description="Name of the zone")
) target: Optional[FirewallTarget] = Field(default=FirewallTarget.DEFAULT,
target: Optional[FirewallTarget] = Field( description="Zone target type")
default=FirewallTarget.DEFAULT, description="Zone target type" services: List[str] = Field(default_factory=list,
) description="List of enabled services")
services: List[str] = Field( ports: List[str] = Field(default_factory=list,
default_factory=list, description="List of enabled services" description="List of enabled ports")
) sources: List[str] = Field(default_factory=list,
ports: List[str] = Field(default_factory=list, description="List of enabled ports") description="List of source addresses")
sources: List[str] = Field( interfaces: List[str] = Field(default_factory=list,
default_factory=list, description="List of source addresses" description="List of network interfaces")
)
interfaces: List[str] = Field(
default_factory=list, description="List of network interfaces"
)
class FirewallService(BaseModel): class FirewallService(BaseModel):
"""Represents a firewalld service configuration""" """Represents a firewalld service configuration"""
name: Annotated[str, StringConstraints(min_length=1)] = Field( name: Annotated[str, StringConstraints(min_length=1)] = Field(
..., description="Name of the service" ..., description="Name of the service")
) ports: List[str] = Field(default_factory=list,
ports: List[str] = Field( description="List of ports used by the service")
default_factory=list, description="List of ports used by the service"
)
protocols: List[str] = Field( protocols: List[str] = Field(
default_factory=list, description="List of protocols used by the service" default_factory=list,
) description="List of protocols used by the service")
modules: List[str] = Field( modules: List[str] = Field(
default_factory=list, default_factory=list,
description="List of kernel modules required by the service", description="List of kernel modules required by the service",
) )
timeout: Optional[int] = Field( timeout: Optional[int] = Field(
default=0, ge=0, description="Timeout in seconds (0 for permanent)" default=0, ge=0, description="Timeout in seconds (0 for permanent)")
)
class FirewallPort(BaseModel): class FirewallPort(BaseModel):
@ -69,8 +62,7 @@ class FirewallPort(BaseModel):
port: int = Field(..., ge=1, le=65535, description="Port number (1-65535)") port: int = Field(..., ge=1, le=65535, description="Port number (1-65535)")
protocol: Protocol = Field(..., description="Network protocol (tcp/udp)") protocol: Protocol = Field(..., description="Network protocol (tcp/udp)")
timeout: Optional[int] = Field( timeout: Optional[int] = Field(
default=0, ge=0, description="Timeout in seconds (0 for permanent)" default=0, ge=0, description="Timeout in seconds (0 for permanent)")
)
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.port}/{self.protocol.value}" return f"{self.port}/{self.protocol.value}"
@ -81,9 +73,11 @@ class FirewallSource(BaseModel):
address: Annotated[ address: Annotated[
str, str,
StringConstraints(pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?:/[0-9]{1,2})?$"), 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") ] = Field(..., description="IP address or network in CIDR notation")
zone: Optional[str] = Field(default=None, description="Zone name for the source") zone: Optional[str] = Field(default=None,
description="Zone name for the source")
@field_validator("address") @field_validator("address")
def validate_ip_address(cls, v): def validate_ip_address(cls, v):
@ -106,10 +100,10 @@ class FirewallRule(BaseModel):
zone: str = Field(..., description="Zone name") zone: str = Field(..., description="Zone name")
source: Optional[FirewallSource] = Field( source: Optional[FirewallSource] = Field(
default=None, description="Source configuration" default=None, description="Source configuration")
) port: Optional[FirewallPort] = Field(default=None,
port: Optional[FirewallPort] = Field(default=None, description="Port configuration") description="Port configuration")
service: Optional[str] = Field(default=None, description="Service name") service: Optional[str] = Field(default=None, description="Service name")
timeout: Optional[int] = Field( timeout: Optional[int] = Field(default=0,
default=0, ge=0, description="Rule timeout in seconds" ge=0,
) description="Rule timeout in seconds")

View file

@ -2,34 +2,87 @@ from typing import List, Optional, Union
from pydantic import BaseModel from pydantic import BaseModel
# Класс для маршрутов IPv4
class IPv4Route(BaseModel): class IPv4Route(BaseModel):
"""
Модель маршрута IPv4
Атрибуты:
destination (str): IP-адрес назначения
prefix (int): Префикс подсети в формате CIDR
next_hop (str): Адрес следующего узла (маршрутизатор)
metric (int): Метрика маршрута
"""
destination: str destination: str
prefix: int prefix: int
next_hop: str next_hop: str
metric: int metric: int
# Класс для маршрутов IPv6
class IPv6Route(BaseModel): class IPv6Route(BaseModel):
"""
Модель маршрута IPv6
Атрибуты:
destination (str): IP-адрес назначения
prefix (int): Префикс подсети (CIDR)
next_hop (str): Адрес следующего узла (маршрутизатор)
metric (int): Метрика маршрута
"""
destination: str destination: str
prefix: int prefix: int
next_hop: str next_hop: str
metric: int metric: int
# Класс для индивидуальной конфигурации IP
class IndividualIPConfig(BaseModel): 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 address: str
routes: List[Union[IPv4Route, IPv6Route]] routes: List[Union[IPv4Route, IPv6Route]]
method: Optional[str] method: Optional[str]
gateway: str gateway: str
dns: List[str] dns: List[str]
# Класс для конфигурации IP
class IPConfig(BaseModel): class IPConfig(BaseModel):
"""
Модель конфигурации IP (IPv4/IPv6)
Атрибуты:
configurations (List[IndividualIPConfig]): Список индивидуальных IP-конфигураций
"""
configurations: List[IndividualIPConfig] configurations: List[IndividualIPConfig]
# Класс для сетевого интерфейса
class NetworkInterface(BaseModel): 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 name: str
type: str type: str
mac_address: str mac_address: str
@ -41,8 +94,20 @@ class NetworkInterface(BaseModel):
container_x: int = 0 container_x: int = 0
container_y: int = 0 container_y: int = 0
# Класс для WiFi устройства
class WiFiDevice(NetworkInterface): 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 ssid: Optional[str] = None
bssid: Optional[str] = None bssid: Optional[str] = None
bitrate: Optional[int] = None bitrate: Optional[int] = None
@ -50,19 +115,53 @@ class WiFiDevice(NetworkInterface):
frequency: Optional[float] = None frequency: Optional[float] = None
mode: Union[str, int] = None mode: Union[str, int] = None
class NetworkInterfacesContainer(BaseModel): class NetworkInterfacesContainer(BaseModel):
"""
Контейнер для группы сетевых интерфейсов с координатами
Атрибуты:
interfaces (List[Union[NetworkInterface, WiFiDevice]]): Список интерфейсов
container_x (int): Положение по оси X
container_y (int): Положение по оси Y
"""
interfaces: List[Union[NetworkInterface, WiFiDevice]] interfaces: List[Union[NetworkInterface, WiFiDevice]]
container_x: int container_x: int
container_y: int container_y: int
class Activation(BaseModel): class Activation(BaseModel):
"""
Модель активированных интерфейсов
Атрибуты:
id (int): Идентификатор ноды активированных интерфейсов
interfaces (List[Union[NetworkInterface, WiFiDevice]]): Активированные интерфейсы
container_x (int): Положение по X на холсте
container_y (int): Положение по Y на холсте
isOpen (bool): Флаг скрытия ноды на холсте
"""
id: int id: int
interfaces: List[Union[NetworkInterface, WiFiDevice]] interfaces: List[Union[NetworkInterface, WiFiDevice]]
container_x: int container_x: int
container_y: int container_y: int
isOpen: bool = True isOpen: bool = True
class Deactivation(BaseModel): class Deactivation(BaseModel):
"""
Модель отключенных интерфейсов
Атрибуты:
id (int): Идентификатор ноды отключенных интерфейсов
interfaces (List[Union[NetworkInterface, WiFiDevice]]): Отключенные интерфейсы
container_x (int): Положение по X на холсте
container_y (int): Положение по Y на холсте
isOpen (bool): Флаг скрытия ноды на холсте
"""
id: int id: int
interfaces: List[Union[NetworkInterface, WiFiDevice]] interfaces: List[Union[NetworkInterface, WiFiDevice]]
container_x: int container_x: int

32
SRV/models/request.py Normal file
View 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
View 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] = []

View file

@ -8,6 +8,7 @@ class Protocol(str, Enum):
""" """
Enum для сетевых протоколов UFW. Enum для сетевых протоколов UFW.
""" """
TCP = "tcp" TCP = "tcp"
UDP = "udp" UDP = "udp"
BOTH = "both" BOTH = "both"
@ -27,6 +28,7 @@ class Action(str, Enum):
DENY: Запретить трафик без уведомления DENY: Запретить трафик без уведомления
REJECT: Запретить трафик с уведомлением отправителю REJECT: Запретить трафик с уведомлением отправителю
""" """
ALLOW = "allow" ALLOW = "allow"
DENY = "deny" DENY = "deny"
REJECT = "reject" REJECT = "reject"
@ -40,12 +42,14 @@ class Direction(str, Enum):
IN: Входящий трафик IN: Входящий трафик
OUT: Исходящий трафик OUT: Исходящий трафик
""" """
IN = "in" IN = "in"
OUT = "out" OUT = "out"
class UFWRule(BaseModel): class UFWRule(BaseModel):
"""Модель правила UFW""" """Модель правила UFW"""
action: Action action: Action
direction: Direction direction: Direction
from_ip: str from_ip: str
@ -70,18 +74,23 @@ class UFWRule(BaseModel):
""" """
if isinstance(v, int): if isinstance(v, int):
if not (1 <= v <= 65535): if not (1 <= v <= 65535):
raise ValueError(f"Порт должен быть в диапазоне 1-65535, получено: {v}") raise ValueError(
f"Порт должен быть в диапазоне 1-65535, получено: {v}")
return v return v
elif isinstance(v, str): elif isinstance(v, str):
try: try:
start, end = map(int, v.split(':')) start, end = map(int, v.split(":"))
if not (1 <= start <= end <= 65535): if not (1 <= start <= end <= 65535):
raise ValueError raise ValueError
return v return v
except (ValueError, TypeError): except (ValueError, TypeError):
raise ValueError(f"Некорректный диапазон портов: {v}. Формат: start:end, где 1 <= start <= end <= 65535") raise ValueError(
f"Некорректный диапазон портов: {v}. Формат: start:end, где 1 <= start <= end <= 65535"
)
else: else:
raise ValueError(f"Порт должен быть числом или строкой с диапазоном, получено: {type(v)}") raise ValueError(
f"Порт должен быть числом или строкой с диапазоном, получено: {type(v)}"
)
@validator("from_ip", "to_ip") @validator("from_ip", "to_ip")
def validate_ip(cls, v): def validate_ip(cls, v):
@ -103,8 +112,11 @@ class UFWRule(BaseModel):
from_ip = ipaddress.ip_network(values["from_ip"], strict=False) from_ip = ipaddress.ip_network(values["from_ip"], strict=False)
to_ip = ipaddress.ip_network(v, strict=False) to_ip = ipaddress.ip_network(v, strict=False)
if isinstance(from_ip, ipaddress.IPv6Network) != isinstance(to_ip, ipaddress.IPv6Network): if isinstance(from_ip, ipaddress.IPv6Network) != isinstance(
raise ValueError("Несовместимые версии IP: нельзя смешивать IPv4 и IPv6 в одном правиле") to_ip, ipaddress.IPv6Network):
raise ValueError(
"Несовместимые версии IP: нельзя смешивать IPv4 и IPv6 в одном правиле"
)
return v return v
@ -139,7 +151,8 @@ class UFWRule(BaseModel):
cmd_parts.extend(["to", self.to_ip]) cmd_parts.extend(["to", self.to_ip])
# Добавляем порт и протокол # Добавляем порт и протокол
port_str = str(self.port) # Работает как с числами, так и с диапазонами port_str = str(
self.port) # Работает как с числами, так и с диапазонами
cmd_parts.extend(["port", port_str]) cmd_parts.extend(["port", port_str])
# Добавляем протокол # Добавляем протокол
@ -183,26 +196,27 @@ class UFWRule(BaseModel):
match = re.match(pattern, line, re.VERBOSE | re.IGNORECASE) match = re.match(pattern, line, re.VERBOSE | re.IGNORECASE)
if not match: if not match:
raise ValueError(f"Невозможно разобрать строку правила UFW: {line}") raise ValueError(
f"Невозможно разобрать строку правила UFW: {line}")
groups = match.groupdict() groups = match.groupdict()
# Преобразуем протокол в enum # Преобразуем протокол в enum
proto_str = groups['protocol'].lower() proto_str = groups["protocol"].lower()
protocol = Protocol.BOTH if proto_str == "tcp/udp" else \ protocol = (Protocol.BOTH if proto_str == "tcp/udp" else
Protocol.UDP if proto_str == "udp" else Protocol.TCP Protocol.UDP if proto_str == "udp" else Protocol.TCP)
# Преобразуем действие в enum (игнорируем регистр) # Преобразуем действие в enum (игнорируем регистр)
action_str = groups['action'].lower() action_str = groups["action"].lower()
action = Action.ALLOW if action_str == "allow" else \ action = (Action.ALLOW if action_str == "allow" else
Action.DENY if action_str == "deny" else Action.REJECT Action.DENY if action_str == "deny" else Action.REJECT)
# Преобразуем направление в enum # Преобразуем направление в enum
direction_str = groups['direction'].lower() direction_str = groups["direction"].lower()
direction = Direction.IN if direction_str == "in" else Direction.OUT direction = Direction.IN if direction_str == "in" else Direction.OUT
# Обрабатываем IP адреса # Обрабатываем IP адреса
from_ip = groups['from_ip'].strip() from_ip = groups["from_ip"].strip()
if from_ip.lower() == "anywhere": if from_ip.lower() == "anywhere":
from_ip = "0.0.0.0/0" from_ip = "0.0.0.0/0"
@ -210,8 +224,8 @@ class UFWRule(BaseModel):
to_ip = "0.0.0.0/0" # По умолчанию anywhere to_ip = "0.0.0.0/0" # По умолчанию anywhere
# Обрабатываем порт # Обрабатываем порт
port = groups['port'] port = groups["port"]
if ':' in port: if ":" in port:
# Оставляем как строку для диапазона # Оставляем как строку для диапазона
port = port.strip() port = port.strip()
else: else:
@ -219,7 +233,8 @@ class UFWRule(BaseModel):
port = int(port) port = int(port)
# Обрабатываем комментарий # Обрабатываем комментарий
comment = groups.get('comment', '').strip() if groups.get('comment') else None comment = groups.get("comment",
"").strip() if groups.get("comment") else None
return cls( return cls(
action=action, action=action,
@ -228,7 +243,7 @@ class UFWRule(BaseModel):
to_ip=to_ip, to_ip=to_ip,
port=port, port=port,
protocol=protocol, protocol=protocol,
comment=comment comment=comment,
) )
@ -241,8 +256,10 @@ class UFWRulesResponse(BaseModel):
rules: Список правил UFW rules: Список правил UFW
total_rules: Общее количество правил total_rules: Общее количество правил
""" """
enabled: bool = Field(..., description="Включен ли UFW") enabled: bool = Field(..., description="Включен ли UFW")
rules: List[UFWRule] = Field(default_factory=list, description="Список правил UFW") rules: List[UFWRule] = Field(default_factory=list,
description="Список правил UFW")
@property @property
def total_rules(self) -> int: def total_rules(self) -> int:
@ -259,5 +276,6 @@ class UFWStatus(BaseModel):
message: Сообщение о статусе message: Сообщение о статусе
last_checked: Время последней проверки статуса last_checked: Время последней проверки статуса
""" """
enabled: bool = Field(..., description="Включен ли UFW") enabled: bool = Field(..., description="Включен ли UFW")
message: str = Field(..., description="Сообщение о статусе") message: str = Field(..., description="Сообщение о статусе")

40
SRV/models/wifi.py Normal file
View 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]

669
SRV/poetry.lock generated
View file

@ -1,4 +1,151 @@
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "aiofiles"
version = "24.1.0"
description = "File support for asyncio."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"},
{file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"},
]
[[package]]
name = "aiohappyeyeballs"
version = "2.6.1"
description = "Happy Eyeballs for asyncio"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"},
{file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"},
]
[[package]]
name = "aiohttp"
version = "3.12.7"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "aiohttp-3.12.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4486f399573c94b223411bc5686b5cdc661f4dd67daece800662356e46b3a2b5"},
{file = "aiohttp-3.12.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67759acb11673c1b976a516f2d69a73433aad70ed04e44ce79eaf0e58219535e"},
{file = "aiohttp-3.12.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8de89889df856101176ccaf570075b73b62ea9d86e11e642d0f20ecd62a34ce8"},
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53ae6140303ab04a7203f8fcb9ca5b2c5abea46e12e8d6f65575d0f197812e22"},
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0d6575df942e7991e1450b731ad9a5726a1116668471a07d749bd9b2cb1f30a7"},
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56d0f622b3595f3aeaefd07aca9d425748fc8bf5e9e502f75a2dad15f2b875b2"},
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c7c848ad08722bfc9da0b9fe5f44cde4fa6499d34ece11462c5b7b1f75a5a1b"},
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b56a4fb31fe82ee58cd8cc157e4fc58d19fba2580b46a62fe7808353bb9b82df"},
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbdb60ab46f696a5e52d98a830b11c034d601bbe2496a82a19d94268257ac63b"},
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8eb5d60790ca3563a376ef297dfac3c4c5ec7a7e180b9fe0314f238813fd2ab0"},
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f1a478d055c77fa549251d8b2a8a850918edbbf9941245ef6edbbb65d924edd7"},
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c4e7155fbdf89084abde1b33f05d681d8ffa0d5d07698d5d76a03ebdeb062848"},
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ed109a3eef13620c8ce57c429119990be08782c346465c265a23052e41e2cf42"},
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:777663011746b37b5df13df7826cb28ebc447b21ac8aa8278b7825404897dd5f"},
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66605ac59c9fbcd4159b0c0cfa239173ab77abc18cf714a1d0569cbabe3c836d"},
{file = "aiohttp-3.12.7-cp310-cp310-win32.whl", hash = "sha256:d909d0b217e85f366bfff45298966ea0dc49d76666fef2eb5777adc5b7aaa292"},
{file = "aiohttp-3.12.7-cp310-cp310-win_amd64.whl", hash = "sha256:362832e0b7c46c7ad3cf2f693061e17f1198f8d7fa4e907c304b3208241161c8"},
{file = "aiohttp-3.12.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:388b5947aa6931ef4ce3ed4edde6853e84980677886992cfadcf733dd06eed63"},
{file = "aiohttp-3.12.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ed5af1cce257cca27a3e920b003b3b397f63418a203064b7d804ea3b45782af"},
{file = "aiohttp-3.12.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f466ae8f9c02993b7d167be685bdbeb527cf254a3cfcc757697e0e336399d0a2"},
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2be095a420a9f9a12eff343d877ae180dd919238b539431af08cef929e874759"},
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b058cf2ba6adba699960d7bc403411c8a99ab5d3e5ea3eb01473638ae7d1a30e"},
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b6a660163b055686dbb0acc961978fd14537eba5d9da6cbdb4dced7a8d3be1a"},
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d741923905f267ad5d5c8f86a56f9d2beac9f32a36c217c5d9ef65cd74fd8ca0"},
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:519f5454b6018158ae0e789b8f6a88726c47dd680982eb318ef3ca4dee727314"},
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4eebfe470e22cc4b374d7e32c07e96d777a5c0fa51f3824de68e697da037ec"},
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:74ff39445f94923cf595e9e6dd602ecbe66b12364e2207e61342b8834417f8da"},
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:77cb9dba16486ecfeac8076763600b9714941e0ff696e53a30e8d408d9a196ca"},
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a7b3b9cbe83e3918a1918b0de274884f17b64224c1c9210a6fb0f7c10d246636"},
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6055f53c70938498884e71ca966abe8e9e7558489e13a7e40b6384dee7230d1d"},
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8493a42d5b2a736c6804239b985feebeea1c60f8fcb46a3607d6dce3c1a42b12"},
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:372f2237cade45f563d973c2a913895f2699a892c0eb11c55c6880b6f0acf219"},
{file = "aiohttp-3.12.7-cp311-cp311-win32.whl", hash = "sha256:41f686749a099b507563a5c0cb4fd77367b05448a2c1758784ad506a28e9e579"},
{file = "aiohttp-3.12.7-cp311-cp311-win_amd64.whl", hash = "sha256:7a3691583470d4397aca70fbf8e0f0778b63a2c2a6a23263bdeeb68395972f29"},
{file = "aiohttp-3.12.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9b9345918f5b5156a5712c37d1d331baf320df67547ea032a49a609b773c3606"},
{file = "aiohttp-3.12.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3091b4883f405dbabeb9ea821a25dec16d03a51c3e0d2752fc3ab48b652bf196"},
{file = "aiohttp-3.12.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:97fd97abd4cf199eff4041d0346a7dc68b60deab177f01de87283be513ffc3ab"},
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a5938973105cd5ff17176e8cb36bc19cac7c82ae7c58c0dbd7e023972d0c708"},
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e506ae5c4c05d1a1e87edd64b994cea2d49385d41d32e1c6be8764f31cf2245c"},
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b780b402e6361c4cfcec252580f5ecdd86cb68376520ac34748d3f8b262dd598"},
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf981bbfb7ff2ebc1b3bfae49d2efe2c51ca1cf3d90867f47c310df65398e85e"},
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94f98e0e5a49f89b252e115844f756c04fc8050f38252a32a3dd994ce8121f10"},
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:410e96cc6824fc4ced9703fb2ac2d06c6190d21fc6f5b588f62b1918628449c1"},
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:43e93987fe9df4349db8deae7c391695538c35e4ba893133c7e823234f6e4537"},
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb3f3dcb59f3e16819a1c7d3fa32e7b87255b661c1e139a1b5940bde270704ab"},
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4a46fe4a4c66b2712059e48a8384eb93565fbe3251af4844860fed846ef4ca75"},
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ad01793164661af70918490ef8efc2c09df7a3c686b6c84ca90a2d69cdbc3911"},
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e85c6833be3f49cead2e7bc79080e5c18d6dab9af32226ab5a01dc20c523e7d9"},
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c9f52149d8249566e72c50c7985c2345521b3b78f84aa86f6f492cd50b14793"},
{file = "aiohttp-3.12.7-cp312-cp312-win32.whl", hash = "sha256:0e1c33ac0f6a396bcefe9c1d52c9d38a051861885a5c102ca5c8298aba0108fa"},
{file = "aiohttp-3.12.7-cp312-cp312-win_amd64.whl", hash = "sha256:b4aed5233a9d13e34e8624ecb798533aa2da97e7048cc69671b7a6d7a2efe7e8"},
{file = "aiohttp-3.12.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:adbb2046600a60e37a54ea9b77b0ddef280029b0a853624a8e9b2b71a037c890"},
{file = "aiohttp-3.12.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76392cbadc1ccc0a8c02098b74c0240d53c644b10a81e1addbc1666dce3cd62a"},
{file = "aiohttp-3.12.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6f04af3bf040dc8fd9b2bc0e465f5aca6fc5349fa08bd7f08142974a2ded21bf"},
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b19763f88f058e9c605f79cde8a800660f7e259162b80982111cc631dfc54bf0"},
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6600550593c440ef29ca2a14b8a52ac91b9f494d85f75294409ec6ad5637476f"},
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c7b83c829be3cddaf958dee8108e09b1502c215e95064d3045015298dbded54a"},
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffa9928fd37061c8e35b85d3f1b4a256d0c3e8cbd421c1d8bd0ab45461b6a838"},
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc8086515dd1016b67db9ccebb7159234226dba99fb6a895a0c9270b644cf525"},
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c19b1de25703560fa64f998dfc3685040b52996056e048b3406c8e97dcfa1e3"},
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6357abdc7a2cfb113274c4f4a7f086bdca36905924953bf7a9e3f6add3aec7c5"},
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:29ff7876ff7e4a8029642334a81891cb5a842f1e405195c2946f697102756670"},
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5e7741c53d473204f89dd26f3b087a5883c742add8d6504d0d7d3ad3ff1cd1b7"},
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:38dc536059cc0624e22273905a1df74b231ac903d73af59ee6e6e3139f05a28b"},
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:82a59cf086396a409d6d2350c122aada07f1f56bb529734994d37bcafc8cf101"},
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7cd6e299292ba085a3642cb4085b393f45bbca45c067182d15e33c2e3473283c"},
{file = "aiohttp-3.12.7-cp313-cp313-win32.whl", hash = "sha256:4acec2b5de65adc469837260be8408d5f53d4c8ae60631be868e9d7eb8563167"},
{file = "aiohttp-3.12.7-cp313-cp313-win_amd64.whl", hash = "sha256:93317649d65cc895ba1fe5384353cb6c44638db39ebb55dabe3dade34a1b1177"},
{file = "aiohttp-3.12.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f85e48970aff5b00af94a5f6311ee0b61eca8bbc8769df39873fc68d747ca609"},
{file = "aiohttp-3.12.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bca9329faa73c42061a67b8b53e6b1d46b73e3411636bfe1d07c58d81067b902"},
{file = "aiohttp-3.12.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e80ef94a0993c7124b69bf1a95b5d26f22f24e5fdc6af22ca143105edde22ed"},
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e1f6e7825d3830ee85ddf5d322300d15053e94c66ff8b3d5e8ef0f152c0f1a"},
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:845a67d26ee9578d20738975591dccd0fcae7104c89cc112316787f9fdfe8b61"},
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1a280e27b2c772a9d69dfd0744929f8628a6b8b6e6e87c0125c8c417501a21"},
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:838a091be15ce619a83896c8485e814215f3383952dd58aec932d0f9ae85a02b"},
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ea0db720f2996f9b799c8ba6fbdd12063add509a81a398cd31a3fb152efae0d"},
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ccd1e07b61c26532f1a7908430c30d687425bbf2d4da26f09bc1f2acf06a5f9"},
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b1f532d312a42397e6f591499acf707cece6462f745c5670bb7c70d1bb963882"},
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:f8fa7c8ee01b54367cafb7e82947e36e57f9cb243d7c4d66e03fb96661b082ae"},
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:81a1ca045593149d3366286c30c57ebb63d2f28feca8ca3fae049c22ed8520c4"},
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9ca179427f7cbd3476eca3bfc229087c112b0418242c5b56f9f0f9c0e681b906"},
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4ee037aec7ccc8777b0f9603085a2c53108368443624f7dc834028b16591541"},
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2804245093897b77736540f75826d35587819e143f0f95e951bfea8eb312bf09"},
{file = "aiohttp-3.12.7-cp39-cp39-win32.whl", hash = "sha256:c8d9b576aa4e1359fcc479532b8a21803840fd61013eec875746b29c3930f073"},
{file = "aiohttp-3.12.7-cp39-cp39-win_amd64.whl", hash = "sha256:1496c9e785d0432a4eae6c059f1d68423fb6264cbdacaff2d9ab1859be66c5bb"},
{file = "aiohttp-3.12.7.tar.gz", hash = "sha256:08bf55b216c779eddb6e41c1841c17d7ddd12776c7d7b36051c0a292a9ca828e"},
]
[package.dependencies]
aiohappyeyeballs = ">=2.5.0"
aiosignal = ">=1.1.2"
attrs = ">=17.3.0"
frozenlist = ">=1.1.1"
multidict = ">=4.5,<7.0"
propcache = ">=0.2.0"
yarl = ">=1.17.0,<2.0"
[package.extras]
speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""]
[[package]]
name = "aiosignal"
version = "1.3.2"
description = "aiosignal: a list of registered asynchronous callbacks"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"},
{file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"},
]
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
@ -34,6 +181,26 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""]
trio = ["trio (>=0.26.1)"] trio = ["trio (>=0.26.1)"]
[[package]]
name = "attrs"
version = "25.3.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"},
{file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"},
]
[package.extras]
benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"]
tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""]
[[package]] [[package]]
name = "bcrypt" name = "bcrypt"
version = "4.3.0" version = "4.3.0"
@ -395,6 +562,120 @@ uvicorn = {version = ">=0.15.0", extras = ["standard"]}
[package.extras] [package.extras]
standard = ["uvicorn[standard] (>=0.15.0)"] standard = ["uvicorn[standard] (>=0.15.0)"]
[[package]]
name = "frozenlist"
version = "1.6.0"
description = "A list-like structure which implements collections.abc.MutableSequence"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e"},
{file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352"},
{file = "frozenlist-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b"},
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc"},
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869"},
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106"},
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24"},
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd"},
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8"},
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c"},
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75"},
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249"},
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769"},
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02"},
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3"},
{file = "frozenlist-1.6.0-cp310-cp310-win32.whl", hash = "sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812"},
{file = "frozenlist-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1"},
{file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d"},
{file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0"},
{file = "frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe"},
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba"},
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595"},
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a"},
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626"},
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff"},
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a"},
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0"},
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606"},
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584"},
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a"},
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1"},
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e"},
{file = "frozenlist-1.6.0-cp311-cp311-win32.whl", hash = "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860"},
{file = "frozenlist-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603"},
{file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1"},
{file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29"},
{file = "frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25"},
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576"},
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8"},
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9"},
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e"},
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590"},
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103"},
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c"},
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821"},
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70"},
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f"},
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046"},
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770"},
{file = "frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc"},
{file = "frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878"},
{file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e"},
{file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117"},
{file = "frozenlist-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4"},
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3"},
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1"},
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c"},
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45"},
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f"},
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85"},
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8"},
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f"},
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f"},
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6"},
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188"},
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e"},
{file = "frozenlist-1.6.0-cp313-cp313-win32.whl", hash = "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4"},
{file = "frozenlist-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd"},
{file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64"},
{file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91"},
{file = "frozenlist-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd"},
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2"},
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506"},
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0"},
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0"},
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e"},
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c"},
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b"},
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad"},
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215"},
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2"},
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911"},
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497"},
{file = "frozenlist-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f"},
{file = "frozenlist-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348"},
{file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:536a1236065c29980c15c7229fbb830dedf809708c10e159b8136534233545f0"},
{file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ed5e3a4462ff25ca84fb09e0fada8ea267df98a450340ead4c91b44857267d70"},
{file = "frozenlist-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e19c0fc9f4f030fcae43b4cdec9e8ab83ffe30ec10c79a4a43a04d1af6c5e1ad"},
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c608f833897501dac548585312d73a7dca028bf3b8688f0d712b7acfaf7fb3"},
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0dbae96c225d584f834b8d3cc688825911960f003a85cb0fd20b6e5512468c42"},
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625170a91dd7261a1d1c2a0c1a353c9e55d21cd67d0852185a5fef86587e6f5f"},
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1db8b2fc7ee8a940b547a14c10e56560ad3ea6499dc6875c354e2335812f739d"},
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4da6fc43048b648275a220e3a61c33b7fff65d11bdd6dcb9d9c145ff708b804c"},
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef8e7e8f2f3820c5f175d70fdd199b79e417acf6c72c5d0aa8f63c9f721646f"},
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa733d123cc78245e9bb15f29b44ed9e5780dc6867cfc4e544717b91f980af3b"},
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ba7f8d97152b61f22d7f59491a781ba9b177dd9f318486c5fbc52cde2db12189"},
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:56a0b8dd6d0d3d971c91f1df75e824986667ccce91e20dca2023683814344791"},
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5c9e89bf19ca148efcc9e3c44fd4c09d5af85c8a7dd3dbd0da1cb83425ef4983"},
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1330f0a4376587face7637dfd245380a57fe21ae8f9d360c1c2ef8746c4195fa"},
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2187248203b59625566cac53572ec8c2647a140ee2738b4e36772930377a533c"},
{file = "frozenlist-1.6.0-cp39-cp39-win32.whl", hash = "sha256:2b8cf4cfea847d6c12af06091561a89740f1f67f331c3fa8623391905e878530"},
{file = "frozenlist-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:1255d5d64328c5a0d066ecb0f02034d086537925f1f04b50b1ae60d37afbf572"},
{file = "frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191"},
{file = "frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68"},
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.14.0" version = "0.14.0"
@ -543,6 +824,37 @@ MarkupSafe = ">=2.0"
[package.extras] [package.extras]
i18n = ["Babel (>=2.7)"] i18n = ["Babel (>=2.7)"]
[[package]]
name = "mac-vendor-lookup"
version = "0.1.12"
description = "Find the vendor for a given MAC address"
optional = false
python-versions = "<4, >=3.5"
groups = ["main"]
files = [
{file = "mac_vendor_lookup-0.1.12-py3-none-any.whl", hash = "sha256:aeec6eac01b07e6558d889b51f475a1e1e938e09cab409a069ab6a43b13cba58"},
{file = "mac_vendor_lookup-0.1.12.tar.gz", hash = "sha256:74e1723e177d4deb02977148d3fa04a7916f4bf93268e2afe3240529272bf80d"},
]
[package.dependencies]
aiofiles = "*"
aiohttp = "*"
[package.extras]
test = ["coverage", "pytest"]
[[package]]
name = "manuf"
version = "1.1.5"
description = "Parser library for Wireshark's OUI database"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "manuf-1.1.5-py2-none-any.whl", hash = "sha256:0d6a34cbc7335e32af9dc75a39e37a8016e4593557e368a3ee2880ae1600819f"},
{file = "manuf-1.1.5.tar.gz", hash = "sha256:741989728c8ffd0d5c5782376c00270fcd48cf7544f58bf9492a0ef674d433cd"},
]
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
version = "3.0.0" version = "3.0.0"
@ -651,6 +963,120 @@ files = [
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
] ]
[[package]]
name = "multidict"
version = "6.4.4"
description = "multidict implementation"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "multidict-6.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8adee3ac041145ffe4488ea73fa0a622b464cc25340d98be76924d0cda8545ff"},
{file = "multidict-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b61e98c3e2a861035aaccd207da585bdcacef65fe01d7a0d07478efac005e028"},
{file = "multidict-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75493f28dbadecdbb59130e74fe935288813301a8554dc32f0c631b6bdcdf8b0"},
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc3c6a37e048b5395ee235e4a2a0d639c2349dffa32d9367a42fc20d399772"},
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87cb72263946b301570b0f63855569a24ee8758aaae2cd182aae7d95fbc92ca7"},
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bbf7bd39822fd07e3609b6b4467af4c404dd2b88ee314837ad1830a7f4a8299"},
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1f7cbd4f1f44ddf5fd86a8675b7679176eae770f2fc88115d6dddb6cefb59bc"},
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5ac9e5bfce0e6282e7f59ff7b7b9a74aa8e5c60d38186a4637f5aa764046ad"},
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4efc31dfef8c4eeb95b6b17d799eedad88c4902daba39ce637e23a17ea078915"},
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fcad2945b1b91c29ef2b4050f590bfcb68d8ac8e0995a74e659aa57e8d78e01"},
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d877447e7368c7320832acb7159557e49b21ea10ffeb135c1077dbbc0816b598"},
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:33a12ebac9f380714c298cbfd3e5b9c0c4e89c75fe612ae496512ee51028915f"},
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0f14ea68d29b43a9bf37953881b1e3eb75b2739e896ba4a6aa4ad4c5b9ffa145"},
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0327ad2c747a6600e4797d115d3c38a220fdb28e54983abe8964fd17e95ae83c"},
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d1a20707492db9719a05fc62ee215fd2c29b22b47c1b1ba347f9abc831e26683"},
{file = "multidict-6.4.4-cp310-cp310-win32.whl", hash = "sha256:d83f18315b9fca5db2452d1881ef20f79593c4aa824095b62cb280019ef7aa3d"},
{file = "multidict-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:9c17341ee04545fd962ae07330cb5a39977294c883485c8d74634669b1f7fe04"},
{file = "multidict-6.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95"},
{file = "multidict-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a"},
{file = "multidict-6.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223"},
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44"},
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065"},
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f"},
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a"},
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2"},
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1"},
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42"},
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e"},
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd"},
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925"},
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c"},
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08"},
{file = "multidict-6.4.4-cp311-cp311-win32.whl", hash = "sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49"},
{file = "multidict-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529"},
{file = "multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2"},
{file = "multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d"},
{file = "multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a"},
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f"},
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93"},
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780"},
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482"},
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1"},
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275"},
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b"},
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2"},
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc"},
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed"},
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740"},
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e"},
{file = "multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b"},
{file = "multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781"},
{file = "multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9"},
{file = "multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf"},
{file = "multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd"},
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15"},
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9"},
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20"},
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b"},
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c"},
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f"},
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69"},
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046"},
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645"},
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0"},
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4"},
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1"},
{file = "multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd"},
{file = "multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373"},
{file = "multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156"},
{file = "multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c"},
{file = "multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e"},
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51"},
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601"},
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de"},
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2"},
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab"},
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0"},
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031"},
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0"},
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26"},
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3"},
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e"},
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd"},
{file = "multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e"},
{file = "multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb"},
{file = "multidict-6.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:603f39bd1cf85705c6c1ba59644b480dfe495e6ee2b877908de93322705ad7cf"},
{file = "multidict-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc60f91c02e11dfbe3ff4e1219c085695c339af72d1641800fe6075b91850c8f"},
{file = "multidict-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:496bcf01c76a70a31c3d746fd39383aad8d685ce6331e4c709e9af4ced5fa221"},
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4219390fb5bf8e548e77b428bb36a21d9382960db5321b74d9d9987148074d6b"},
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef4e9096ff86dfdcbd4a78253090ba13b1d183daa11b973e842465d94ae1772"},
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49a29d7133b1fc214e818bbe025a77cc6025ed9a4f407d2850373ddde07fd04a"},
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e32053d6d3a8b0dfe49fde05b496731a0e6099a4df92154641c00aa76786aef5"},
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc403092a49509e8ef2d2fd636a8ecefc4698cc57bbe894606b14579bc2a955"},
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5363f9b2a7f3910e5c87d8b1855c478c05a2dc559ac57308117424dfaad6805c"},
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e543a40e4946cf70a88a3be87837a3ae0aebd9058ba49e91cacb0b2cd631e2b"},
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:60d849912350da557fe7de20aa8cf394aada6980d0052cc829eeda4a0db1c1db"},
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:19d08b4f22eae45bb018b9f06e2838c1e4b853c67628ef8ae126d99de0da6395"},
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d693307856d1ef08041e8b6ff01d5b4618715007d288490ce2c7e29013c12b9a"},
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fad6daaed41021934917f4fb03ca2db8d8a4d79bf89b17ebe77228eb6710c003"},
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c10d17371bff801af0daf8b073c30b6cf14215784dc08cd5c43ab5b7b8029bbc"},
{file = "multidict-6.4.4-cp39-cp39-win32.whl", hash = "sha256:7e23f2f841fcb3ebd4724a40032d32e0892fbba4143e43d2a9e7695c5e50e6bd"},
{file = "multidict-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d7b50b673ffb4ff4366e7ab43cf1f0aef4bd3608735c5fbdf0bdb6f690da411"},
{file = "multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac"},
{file = "multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8"},
]
[[package]] [[package]]
name = "netaddr" name = "netaddr"
version = "1.3.0" version = "1.3.0"
@ -749,6 +1175,114 @@ bcrypt = ["bcrypt (>=3.1.0)"]
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
totp = ["cryptography"] totp = ["cryptography"]
[[package]]
name = "propcache"
version = "0.3.1"
description = "Accelerated property cache"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"},
{file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"},
{file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"},
{file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"},
{file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"},
{file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"},
{file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"},
{file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"},
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"},
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"},
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"},
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"},
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"},
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"},
{file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"},
{file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"},
{file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"},
{file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"},
{file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"},
{file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"},
{file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"},
{file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"},
{file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"},
{file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"},
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"},
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"},
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"},
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"},
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"},
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"},
{file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"},
{file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"},
{file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"},
{file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"},
{file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"},
{file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"},
{file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"},
{file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"},
{file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"},
{file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"},
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"},
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"},
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"},
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"},
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"},
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"},
{file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"},
{file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"},
{file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"},
{file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"},
{file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"},
{file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"},
{file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"},
{file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"},
{file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"},
{file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"},
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"},
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"},
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"},
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"},
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"},
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"},
{file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"},
{file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"},
{file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"},
{file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"},
{file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"},
{file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"},
{file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"},
{file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"},
{file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"},
{file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"},
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"},
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"},
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"},
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"},
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"},
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"},
{file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"},
{file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"},
{file = "propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6"},
{file = "propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf"},
{file = "propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c"},
{file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894"},
{file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035"},
{file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908"},
{file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5"},
{file = "propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5"},
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7"},
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641"},
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294"},
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf"},
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c"},
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe"},
{file = "propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64"},
{file = "propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566"},
{file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"},
{file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"},
]
[[package]] [[package]]
name = "pyasn1" name = "pyasn1"
version = "0.4.8" version = "0.4.8"
@ -1059,6 +1593,18 @@ files = [
{file = "python_pam-2.0.2-py3-none-any.whl", hash = "sha256:4ac51dd8953ac59aa45505882b565eef6a22e0423dcf25d63369902080416c20"}, {file = "python_pam-2.0.2-py3-none-any.whl", hash = "sha256:4ac51dd8953ac59aa45505882b565eef6a22e0423dcf25d63369902080416c20"},
] ]
[[package]]
name = "pywifi"
version = "1.1.12"
description = "A cross-platform module for manipulating WiFi devices."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
groups = ["main"]
files = [
{file = "pywifi-1.1.12-py3-none-any.whl", hash = "sha256:d1b79d9ff054eef1aca4ab97f2641650d001d6740bfa7666a7d9506bcfb8459b"},
{file = "pywifi-1.1.12.tar.gz", hash = "sha256:6da15de5d573635a461a95919920788b6f61748e1bd93a5e3504f456cf2e8bcc"},
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.2" version = "6.0.2"
@ -1569,7 +2115,126 @@ files = [
{file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"}, {file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"},
] ]
[[package]]
name = "yarl"
version = "1.20.0"
description = "Yet another URL library"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"},
{file = "yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62"},
{file = "yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569"},
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe"},
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195"},
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10"},
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634"},
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2"},
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a"},
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867"},
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995"},
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487"},
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2"},
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61"},
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19"},
{file = "yarl-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d"},
{file = "yarl-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076"},
{file = "yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3"},
{file = "yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a"},
{file = "yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2"},
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e"},
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9"},
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a"},
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2"},
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2"},
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8"},
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902"},
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791"},
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f"},
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da"},
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4"},
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5"},
{file = "yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6"},
{file = "yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb"},
{file = "yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f"},
{file = "yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e"},
{file = "yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e"},
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33"},
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58"},
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f"},
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae"},
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018"},
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672"},
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8"},
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7"},
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594"},
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6"},
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1"},
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b"},
{file = "yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64"},
{file = "yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c"},
{file = "yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f"},
{file = "yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3"},
{file = "yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d"},
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0"},
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501"},
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc"},
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d"},
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0"},
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a"},
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2"},
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9"},
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5"},
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877"},
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e"},
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384"},
{file = "yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62"},
{file = "yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c"},
{file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051"},
{file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d"},
{file = "yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229"},
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1"},
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb"},
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00"},
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de"},
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5"},
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a"},
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9"},
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145"},
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda"},
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f"},
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd"},
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f"},
{file = "yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac"},
{file = "yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe"},
{file = "yarl-1.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914"},
{file = "yarl-1.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc"},
{file = "yarl-1.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26"},
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94"},
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d"},
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c"},
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c"},
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a"},
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656"},
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c"},
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64"},
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20"},
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa"},
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5"},
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0"},
{file = "yarl-1.20.0-cp39-cp39-win32.whl", hash = "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8"},
{file = "yarl-1.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7"},
{file = "yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124"},
{file = "yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"},
]
[package.dependencies]
idna = ">=2.0"
multidict = ">=4.0"
propcache = ">=0.2.1"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "a9465e2172fc3fe6ee66a3df67343b3e245e723d4244d8e85534df3ed1696369" content-hash = "6dfae73c0cdec175259066cdcf6fe6bceefc5ddfc84252b6093d1abf3ec82d58"

View file

@ -10,7 +10,6 @@ packages = [
{include = "models"}, {include = "models"},
{include = "services"}, {include = "services"},
{include = "core"}, {include = "core"},
{include = "tests"},
{include = "storage"}, {include = "storage"},
] ]
@ -36,6 +35,9 @@ storage = "^0.0.4.3"
passlib = {extras = ["bcrypt"], version = "^1.7.4"} passlib = {extras = ["bcrypt"], version = "^1.7.4"}
python-multipart = "^0.0.20" python-multipart = "^0.0.20"
bcrypt = "^4.3.0" bcrypt = "^4.3.0"
mac-vendor-lookup = "^0.1.12"
pywifi = "^1.1.12"
manuf = "^1.1.5"
[build-system] [build-system]

View file

@ -3,14 +3,21 @@ from scapy.sendrecv import srp
import netifaces import netifaces
from netaddr import IPNetwork from netaddr import IPNetwork
class NetworkScanner: class NetworkScanner:
"""Иницилазицая сетевого сканера""" """Иницилазицая сетевого сканера"""
def __init__(self, interface=None): def __init__(self, interface=None):
"""Инициализация параметров интерфейсов""" """Инициализация параметров интерфейсов"""
self.interface = interface or netifaces.gateways()['default'][netifaces.AF_INET][1] self.interface = (
self.ip_info = netifaces.ifaddresses(self.interface).get(netifaces.AF_INET, []) interface or netifaces.gateways()["default"][netifaces.AF_INET][1])
self.networks = [IPNetwork(f"{ip['addr']}/{ip['netmask']}") for ip in self.ip_info] self.ip_info = netifaces.ifaddresses(self.interface).get(
self.local_mac = netifaces.ifaddresses(self.interface)[netifaces.AF_LINK][0]['addr'] 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): def _create_arp_packet(self, target_ip):
"""Создание пакета ARP для отправки""" """Создание пакета ARP для отправки"""
@ -28,9 +35,12 @@ class NetworkScanner:
clients = [] clients = []
for _ in range(attempts): for _ in range(attempts):
result = srp(packet, timeout=timeout, iface=self.interface, verbose=0)[0] result = srp(packet,
timeout=timeout,
iface=self.interface,
verbose=0)[0]
for _, received in result: for _, received in result:
client = {'ip': received.psrc, 'mac': received.hwsrc} client = {"ip": received.psrc, "mac": received.hwsrc}
if client not in clients: # Избегаем дублирования клиентов if client not in clients: # Избегаем дублирования клиентов
clients.append(client) clients.append(client)
@ -38,7 +48,7 @@ class NetworkScanner:
# Добавляет все локальные IP-адреса интерфейса как клиенты # Добавляет все локальные IP-адреса интерфейса как клиенты
for ip_info in self.ip_info: for ip_info in self.ip_info:
local_client = {'ip': ip_info['addr'], 'mac': self.local_mac} local_client = {"ip": ip_info["addr"], "mac": self.local_mac}
if local_client not in all_clients: if local_client not in all_clients:
all_clients.append(local_client) all_clients.append(local_client)
@ -50,9 +60,10 @@ class NetworkScanner:
"""Лист адресов клиентов""" """Лист адресов клиентов"""
print("Доступные устройства в сети:") print("Доступные устройства в сети:")
print("IP" + " " * 18 + "MAC") print("IP" + " " * 18 + "MAC")
for client in sorted(clients, key=lambda x: x['ip']): for client in sorted(clients, key=lambda x: x["ip"]):
print(f"{client['ip']:<16} {client['mac']}") print(f"{client['ip']:<16} {client['mac']}")
if __name__ == "__main__": if __name__ == "__main__":
scanner = NetworkScanner() scanner = NetworkScanner()
clients = scanner.scan_network() clients = scanner.scan_network()

View file

@ -26,8 +26,10 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
class AuthService: class AuthService:
"""Сервис для регистрации, авторизации и управления сессиями""" """Сервис для регистрации, авторизации и управления сессиями"""
def __init__(self): def __init__(self):
self.storage = JsonStorage() self.storage = JsonStorage()
self.session_storage = SessionStorage() self.session_storage = SessionStorage()
@ -35,7 +37,8 @@ class AuthService:
def create_access_token(self, data: dict) -> str: def create_access_token(self, data: dict) -> str:
"""Создаёт токен""" """Создаёт токен"""
to_encode = data.copy() to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) expire = datetime.utcnow() + timedelta(
minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire}) to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt return encoded_jwt
@ -57,10 +60,14 @@ class AuthService:
db_user = self.storage.create_user(user) db_user = self.storage.create_user(user)
return db_user return db_user
except ValueError as e: except ValueError as e:
logger.error(f"Ошибка при регистрации пользователя {user.username}: {str(e)}") logger.error(
f"Ошибка при регистрации пользователя {user.username}: {str(e)}"
)
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Ошибка при регистрации пользователя {user.username}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при регистрации пользователя {user.username}: {type(e).__name__} - {str(e)}"
)
raise HTTPException(status_code=500, detail="Ошибка регистрации") raise HTTPException(status_code=500, detail="Ошибка регистрации")
def login(self, user: UserLogin, client_id: str) -> dict: def login(self, user: UserLogin, client_id: str) -> dict:
@ -69,24 +76,41 @@ class AuthService:
db_user = self.storage.get_user_by_username(user.username) db_user = self.storage.get_user_by_username(user.username)
if not db_user: if not db_user:
logger.error(f"Пользователь {user.username} не найден") logger.error(f"Пользователь {user.username} не найден")
raise HTTPException(status_code=401, detail="Неверный логин или пароль") raise HTTPException(status_code=401,
if not self.storage.verify_password(user.password, db_user["password_hash"]): detail="Неверный логин или пароль")
logger.error(f"Неверный пароль для пользователя {user.username}") if not self.storage.verify_password(user.password,
raise HTTPException(status_code=401, detail="Неверный логин или пароль") db_user["password_hash"]):
access_token = self.create_access_token(data={"sub": user.username}) logger.error(
session_id = self.session_storage.create_session(user.username, access_token, client_id) 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} успешно авторизован") logger.info(f"Пользователь {user.username} успешно авторизован")
return { return {
"access_token": access_token, "access_token":
"token_type": "bearer", access_token,
"session_id": session_id, "token_type":
"user": UserResponse(id=db_user["id"], username=db_user["username"], is_admin=db_user["is_admin"]) "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: except Exception as e:
logger.error(f"Ошибка при авторизации пользователя {user.username}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при авторизации пользователя {user.username}: {type(e).__name__} - {str(e)}"
)
raise HTTPException(status_code=500, detail="Ошибка авторизации") raise HTTPException(status_code=500, detail="Ошибка авторизации")
def get_current_user(self, token: str = Depends(oauth2_scheme)) -> UserResponse: def get_current_user(
self, token: str = Depends(oauth2_scheme)) -> UserResponse:
"""Получает текущего пользователя по токену""" """Получает текущего пользователя по токену"""
credentials = self.verify_token(token) credentials = self.verify_token(token)
if not credentials: if not credentials:
@ -96,24 +120,36 @@ class AuthService:
db_user = self.storage.get_user_by_username(username) db_user = self.storage.get_user_by_username(username)
if not db_user: if not db_user:
logger.error(f"Пользователь {username} не найден") logger.error(f"Пользователь {username} не найден")
raise HTTPException(status_code=401, detail="Пользователь не найден") raise HTTPException(status_code=401,
return UserResponse(id=db_user["id"], username=db_user["username"], is_admin=db_user["is_admin"]) 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: def get_user_by_session(self, client_id: str) -> dict:
"""Получает пользователя и токен по client_id""" """Получает пользователя и токен по client_id"""
session = self.session_storage.get_latest_session(client_id) session = self.session_storage.get_latest_session(client_id)
if not session: if not session:
logger.error(f"Сессия для client_id {client_id} не найдена или истекла") logger.error(
raise HTTPException(status_code=401, detail="Сессия недействительна") f"Сессия для client_id {client_id} не найдена или истекла")
raise HTTPException(status_code=401,
detail="Сессия недействительна")
username = session["username"] username = session["username"]
token = session["token"] token = session["token"]
db_user = self.storage.get_user_by_username(username) db_user = self.storage.get_user_by_username(username)
if not db_user: if not db_user:
logger.error(f"Пользователь {username} не найден") logger.error(f"Пользователь {username} не найден")
raise HTTPException(status_code=401, detail="Пользователь не найден") raise HTTPException(status_code=401,
detail="Пользователь не найден")
return { return {
"user": UserResponse(id=db_user["id"], username=db_user["username"], is_admin=db_user["is_admin"]), "user":
"access_token": token UserResponse(
id=db_user["id"],
username=db_user["username"],
is_admin=db_user["is_admin"],
),
"access_token":
token,
} }
def logout(self, client_id: str): def logout(self, client_id: str):
@ -122,8 +158,12 @@ class AuthService:
session = self.session_storage.get_latest_session(client_id) session = self.session_storage.get_latest_session(client_id)
if session: if session:
self.session_storage.delete_session(session["session_id"]) self.session_storage.delete_session(session["session_id"])
logger.info(f"Пользователь успешно вышел из системы для client_id {client_id}") logger.info(
f"Пользователь успешно вышел из системы для client_id {client_id}"
)
return {"message": "Успешный выход из системы"} return {"message": "Успешный выход из системы"}
except Exception as e: except Exception as e:
logger.error(f"Ошибка при выходе из системы: {type(e).__name__} - {str(e)}") logger.error(
raise HTTPException(status_code=500, detail="Ошибка при выходе из системы") f"Ошибка при выходе из системы: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500,
detail="Ошибка при выходе из системы")

View file

@ -0,0 +1,106 @@
import subprocess
import logging
from typing import Tuple, Optional
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
class WifiConnectionService:
def __init__(self):
self.interface = self._get_wifi_interface()
def _get_wifi_interface(self) -> str:
"""Автоматически определяет Wi-Fi-интерфейс."""
try:
result = subprocess.run(["nmcli", "radio", "wifi"], capture_output=True, text=True)
if "enabled" in result.stdout.lower():
interfaces = subprocess.run(["nmcli", "device", "status"], capture_output=True, text=True).stdout
for line in interfaces.splitlines():
if "wifi" in line.lower() and "connected" in line.lower():
iface = line.split()[0]
logger.info(f"Используется Wi-Fi-интерфейс: {iface}")
return iface
logger.warning("Wi-Fi-интерфейс не найден или не подключён")
return "wlan0"
logger.error("Wi-Fi радио отключено")
return ""
except Exception as e:
logger.error(f"Ошибка при определении Wi-Fi-интерфейса: {str(e)}")
return ""
def connect(self, ssid: str, password: str) -> Tuple[bool, Optional[str]]:
"""Подключается к Wi-Fi сети по SSID и паролю."""
try:
if not self.interface:
return False, "Wi-Fi-интерфейс не доступен"
result = subprocess.run(["nmcli", "-f", "SSID", "device", "wifi", "list"], capture_output=True, text=True)
if ssid not in result.stdout:
return False, f"Сеть с SSID {ssid} не найдена в списке доступных сетей"
cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]
process = subprocess.run(cmd, capture_output=True, text=True, check=False)
if process.returncode != 0:
error_msg = process.stderr.lower()
if "secrets were required" in error_msg or "authentication failed" in error_msg or "802-11-wireless-security.psk: недействительное свойство" in process.stderr:
return False, f"Ошибка подключения к {ssid}: Неверный пароль"
elif "no network with ssid" in error_msg:
return False, f"Ошибка подключения к {ssid}: Сеть с SSID {ssid} недоступна или неверно указана"
else:
return False, f"Ошибка подключения к {ssid}: {process.stderr}"
logger.info(f"Успешное подключение к сети {ssid}")
return True, None
except subprocess.CalledProcessError as e:
logger.error(f"Ошибка подключения к {ssid}: {e.stderr}")
error_msg = e.stderr.lower()
if "secrets were required" in error_msg or "authentication failed" in error_msg or "802-11-wireless-security.psk: недействительное свойство" in e.stderr:
return False, f"Ошибка подключения к {ssid}: Неверный пароль"
elif "no network with ssid" in error_msg:
return False, f"Ошибка подключения к {ssid}: Сеть с SSID {ssid} недоступна или неверно указана"
return False, f"Ошибка подключения к {ssid}: {e.stderr}"
except Exception as e:
logger.error(f"Неизвестная ошибка при подключении к {ssid}: {str(e)}")
return False, f"Ошибка подключения к {ssid}: Неизвестная ошибка - {str(e)}"
def disconnect(self, ssid: str) -> Tuple[bool, Optional[str]]:
"""Отключается от Wi-Fi сети по указанному SSID."""
try:
if not self.interface:
return False, "Wi-Fi-интерфейс не доступен"
result = subprocess.run(["nmcli", "connection", "show", "--active"], capture_output=True, text=True)
active_connections = result.stdout.strip().splitlines()
if not active_connections:
logger.info(f"Нет активных подключений для SSID {ssid}")
return True, f"Нет активного подключения для SSID {ssid}"
for conn in active_connections:
conn_details = conn.split()
if len(conn_details) > 1:
conn_name = conn_details[0]
conn_info = subprocess.run(["nmcli", "connection", "show", conn_name], capture_output=True, text=True).stdout
if "802-11-wireless.ssid:" in conn_info and ssid in conn_info:
subprocess.run(["nmcli", "connection", "down", conn_name], capture_output=True, text=True, check=True)
logger.info(f"Отключено соединение с SSID {ssid}")
return True, None
logger.warning(f"Активное подключение с SSID {ssid} не найдено")
return True, f"Активное подключение с SSID {ssid} не найдено"
except subprocess.CalledProcessError as e:
logger.error(f"Ошибка при отключении от {ssid}: {e.stderr}")
return False, f"Ошибка отключения от {ssid}: {e.stderr}"
except Exception as e:
logger.error(f"Неизвестная ошибка при отключении от {ssid}: {str(e)}")
return False, f"Ошибка отключения от {ssid}: {str(e)}"
def get_status(self) -> Tuple[bool, Optional[str]]:
"""Проверяет статус текущего подключения к Wi-Fi."""
try:
if not self.interface:
return False, "Wi-Fi-интерфейс не доступен"
result = subprocess.run(["nmcli", "connection", "show", "--active"], capture_output=True, text=True)
if result.stdout.strip():
ssid = result.stdout.splitlines()[1].split()[0]
logger.info(f"Текущая сеть: {ssid}")
return True, ssid
return False, "Не подключено к Wi-Fi"
except Exception as e:
logger.error(f"Ошибка при проверке статуса: {str(e)}")
return False, f"Ошибка проверки статуса: {str(e)}"

View file

@ -7,11 +7,11 @@ DEBUG = True
class FirewalldService: class FirewalldService:
def __init__(self): def __init__(self):
self.bus = SystemBus() self.bus = SystemBus()
self.proxy = self.bus.get( self.proxy = self.bus.get("org.fedoraproject.FirewallD1",
"org.fedoraproject.FirewallD1", "/org/fedoraproject/FirewallD1" "/org/fedoraproject/FirewallD1")
)
def get_zones(self) -> List[str]: def get_zones(self) -> List[str]:
"""Get list of all zones.""" """Get list of all zones."""
@ -34,12 +34,16 @@ class FirewalldService:
except Exception as e: except Exception as e:
raise Exception(f"Failed to set default zone to {zone}: {str(e)}") raise Exception(f"Failed to set default zone to {zone}: {str(e)}")
def add_service(self, zone: str, service: str, timeout: Optional[int] = 0) -> None: def add_service(self,
zone: str,
service: str,
timeout: Optional[int] = 0) -> None:
"""Add a service to a zone.""" """Add a service to a zone."""
try: try:
self.proxy.addService(zone, service, timeout) self.proxy.addService(zone, service, timeout)
except Exception as e: except Exception as e:
raise Exception(f"Failed to add service {service} to zone {zone}: {str(e)}") raise Exception(
f"Failed to add service {service} to zone {zone}: {str(e)}")
def remove_service(self, zone: str, service: str) -> None: def remove_service(self, zone: str, service: str) -> None:
"""Remove a service from a zone.""" """Remove a service from a zone."""
@ -55,22 +59,23 @@ class FirewalldService:
try: try:
return self.proxy.getServices(zone) return self.proxy.getServices(zone)
except Exception as e: except Exception as e:
raise Exception(f"Failed to get services for zone {zone}: {str(e)}") raise Exception(
f"Failed to get services for zone {zone}: {str(e)}")
def add_port( def add_port(self,
self, zone: str, port: int, protocol: str, timeout: Optional[int] = 0 zone: str,
) -> None: port: int,
protocol: str,
timeout: Optional[int] = 0) -> None:
"""Add a port to a zone.""" """Add a port to a zone."""
try: try:
if DEBUG: if DEBUG:
print(f"Adding port {port}/{protocol} to zone {zone}") print(f"Adding port {port}/{protocol} to zone {zone}")
print(f"Timeout: {timeout}") print(f"Timeout: {timeout}")
# Get the zone interface # Get the zone interface
zone_proxy = self.bus.get( zone_proxy = self.bus.get("org.fedoraproject.FirewallD1",
"org.fedoraproject.FirewallD1", "/org/fedoraproject/FirewallD1")
"/org/fedoraproject/FirewallD1" zone_iface = zone_proxy["org.fedoraproject.FirewallD1.zone"]
)
zone_iface = zone_proxy['org.fedoraproject.FirewallD1.zone']
# Convert port to string # Convert port to string
port_str = str(port) port_str = str(port)
# Convert timeout to integer # Convert timeout to integer
@ -106,7 +111,8 @@ class FirewalldService:
try: try:
self.proxy.addSource(zone, source) self.proxy.addSource(zone, source)
except Exception as e: except Exception as e:
raise Exception(f"Failed to add source {source} to zone {zone}: {str(e)}") raise Exception(
f"Failed to add source {source} to zone {zone}: {str(e)}")
def remove_source(self, zone: str, source: str) -> None: def remove_source(self, zone: str, source: str) -> None:
"""Remove a source from a zone.""" """Remove a source from a zone."""
@ -114,8 +120,7 @@ class FirewalldService:
self.proxy.removeSource(zone, source) self.proxy.removeSource(zone, source)
except Exception as e: except Exception as e:
raise Exception( raise Exception(
f"Failed to remove source {source} from zone {zone}: {str(e)}" f"Failed to remove source {source} from zone {zone}: {str(e)}")
)
def get_sources(self, zone: str) -> List[str]: def get_sources(self, zone: str) -> List[str]:
"""Get list of sources in a zone.""" """Get list of sources in a zone."""
@ -140,4 +145,5 @@ class FirewalldService:
"sources": self.get_sources(zone), "sources": self.get_sources(zone),
} }
except Exception as e: except Exception as e:
raise Exception(f"Failed to get zone settings for {zone}: {str(e)}") raise Exception(
f"Failed to get zone settings for {zone}: {str(e)}")

View file

@ -1,14 +1,21 @@
from sdbus_async.networkmanager import NetworkManager, NetworkManagerSettings, NetworkConnectionSettings, NetworkDeviceGeneric, ActiveConnection
import asyncio import asyncio
from sdbus_async.networkmanager.settings import ConnectionProfile
from typing import Dict, Any from typing import Dict, Any
from sdbus_async.networkmanager import (
NetworkManager,
NetworkManagerSettings,
NetworkConnectionSettings,
NetworkDeviceGeneric,
)
import sdbus import sdbus
from sdbus_async.networkmanager.settings import ConnectionProfile
from services.arp_service import NetworkScanner from services.arp_service import NetworkScanner
class IPService: class IPService:
""" """
Сервис проверки доступности IP-адреса и применения настроек сети Сервис проверки доступности IP-адреса и применения настроек сети
""" """
def __init__(self): def __init__(self):
self.arp_scanner = NetworkScanner() self.arp_scanner = NetworkScanner()
self.bus = sdbus.sd_bus_open_system() self.bus = sdbus.sd_bus_open_system()
@ -18,7 +25,7 @@ class IPService:
"""Перезапускает соединение с системной шиной D-Bus""" """Перезапускает соединение с системной шиной D-Bus"""
new_bus = sdbus.sd_bus_open_system() new_bus = sdbus.sd_bus_open_system()
sdbus.set_default_bus(new_bus) sdbus.set_default_bus(new_bus)
if hasattr(self, 'bus') and self.bus is not None: if hasattr(self, "bus") and self.bus is not None:
self.bus = None self.bus = None
self.bus = new_bus self.bus = new_bus
@ -27,27 +34,33 @@ class IPService:
self._reset_bus() self._reset_bus()
clients = self.arp_scanner.scan_network() clients = self.arp_scanner.scan_network()
new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"] new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"]
if any(client['ip'] == new_ip for client in clients): if any(client["ip"] == new_ip for client in clients):
return {"available": False, "message": f"IP address {new_ip} уже занят."} return {
return {"available": True, "message": "Network profile settings are available for use."} "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: async def apply_ip_config(self, args: Dict[str, Any]) -> None:
"""Применяет проверенные настройки IP""" """Применяет проверенные настройки IP"""
self._reset_bus() self._reset_bus()
nm = NetworkManager(self.bus) nm = NetworkManager()
settings = NetworkManagerSettings(self.bus) settings = NetworkManagerSettings()
connection_paths = await settings.get_connections_by_id(args["connection_id"]) connection_paths = await settings.get_connections_by_id(
args["connection_id"])
if not connection_paths: if not connection_paths:
raise ValueError(f"Подключение {args['connection_id']} не найдено") raise ValueError(f"Подключение {args['connection_id']} не найдено")
connection = NetworkConnectionSettings(connection_paths[0], self.bus) connection = NetworkConnectionSettings(connection_paths[0])
profile = await connection.get_profile() profile = await connection.get_profile()
settings_dict = profile.to_settings_dict() settings_dict = profile.to_settings_dict()
# Обновляем настройки IPv4
for key_path, value in args["ipv4_updates"].items(): for key_path, value in args["ipv4_updates"].items():
section, *props = key_path.split('.') section, *props = key_path.split(".")
current = settings_dict[section] current = settings_dict[section]
for prop in props[:-1]: for prop in props[:-1]:
current = current.setdefault(prop, {}) current = current.setdefault(prop, {})
@ -57,56 +70,17 @@ class IPService:
await connection.update(new_profile.to_dbus()) await connection.update(new_profile.to_dbus())
print(f"Обновлены настройки для {args['connection_id']}:") print(f"Обновлены настройки для {args['connection_id']}:")
# Проверяем interface-name interface_name = settings_dict["connection"]["interface-name"]
interface_name = settings_dict['connection'].get('interface-name')
if not interface_name:
# Ищем устройство, связанное с connection_id через активное соединение
devices = await nm.get_devices() devices = await nm.get_devices()
for device_path in devices: for device_path in devices:
device = NetworkDeviceGeneric(device_path, self.bus) device = NetworkDeviceGeneric(device_path)
active_connection = await device.active_connection
if active_connection != '/':
active_conn = ActiveConnection(active_connection, self.bus)
conn_path = await active_conn.connection # Путь к настройкам соединения
if conn_path != '/':
conn_settings = NetworkConnectionSettings(conn_path, self.bus)
active_profile = await conn_settings.get_profile()
if active_profile.to_settings_dict()['connection']['id'] == args["connection_id"]:
interface_name = await device.interface
settings_dict['connection']['interface-name'] = interface_name
new_profile = ConnectionProfile.from_settings_dict(settings_dict)
await connection.update(new_profile.to_dbus())
print(f"Добавлен interface-name: {interface_name} для {args['connection_id']}")
break
if not interface_name:
# Если не нашли активное соединение, пробуем получить интерфейс из настроек устройства
devices = await nm.get_devices()
for device_path in devices:
device = NetworkDeviceGeneric(device_path, self.bus)
applied_connection = await device.get_applied_connection()
connection_settings = applied_connection[0]
conn_id = connection_settings.get('connection', {}).get('id', ('s', ''))[1]
if conn_id == args["connection_id"]:
interface_name = await device.interface
settings_dict['connection']['interface-name'] = interface_name
new_profile = ConnectionProfile.from_settings_dict(settings_dict)
await connection.update(new_profile.to_dbus())
print(f"Добавлен interface-name: {interface_name} для {args['connection_id']}")
break
if not interface_name:
raise ValueError(f"Не удалось определить interface-name для {args['connection_id']}")
# Переподключаем устройство
devices = await nm.get_devices()
for device_path in devices:
device = NetworkDeviceGeneric(device_path, self.bus)
if await device.interface == interface_name: if await device.interface == interface_name:
await device.disconnect() await device.disconnect()
print(f"Отключён от {interface_name}") print(f"Отключён от {interface_name}")
await nm.activate_connection(connection=connection_paths[0], device=device_path) await nm.activate_connection(connection=connection_paths[0],
device=device_path)
print(f"Переподключён к {args['connection_id']}") print(f"Переподключён к {args['connection_id']}")
return return
raise RuntimeError(f"Устройство для интерфейса {interface_name} не найдено") raise RuntimeError(
f"Устройство для интерфейса {interface_name} не найдено")

View file

@ -7,7 +7,7 @@ from sdbus_async.networkmanager import (
NetworkManagerSettings, NetworkManagerSettings,
NetworkDeviceWireless, NetworkDeviceWireless,
AccessPoint, AccessPoint,
DeviceType DeviceType,
) )
from sdbus import sd_bus_open_system from sdbus import sd_bus_open_system
from models.network import ( from models.network import (
@ -17,10 +17,11 @@ from models.network import (
IPConfig, IPConfig,
IndividualIPConfig, IndividualIPConfig,
NetworkInterfacesContainer, NetworkInterfacesContainer,
WiFiDevice WiFiDevice,
) )
import subprocess import subprocess
class NetworkService: class NetworkService:
"""Сервис для взаимодействия с NetworkManager через D-Bus""" """Сервис для взаимодействия с NetworkManager через D-Bus"""
@ -29,76 +30,99 @@ class NetworkService:
self.nm = NetworkManager(self.system_bus) self.nm = NetworkManager(self.system_bus)
self.nm_settings = NetworkManagerSettings(self.system_bus) self.nm_settings = NetworkManagerSettings(self.system_bus)
async def get_ip_method(self, device: NetworkDeviceGeneric, ip_type: str) -> str: async def get_ip_method(self, device: NetworkDeviceGeneric,
ip_type: str) -> str:
"""Получает метод IP для устройства""" """Получает метод IP для устройства"""
try: try:
config = await device.get_applied_connection() config = await device.get_applied_connection()
settings = config[0].get(ip_type, {}) settings = config[0].get(ip_type, {})
return str(settings.get("method", ('s', 'unknown'))[1]) return str(settings.get("method", ("s", "unknown"))[1])
except Exception: except Exception:
return 'unknown' return "unknown"
async def get_connection_name(self, device: NetworkDeviceGeneric) -> str: async def get_connection_name(self, device: NetworkDeviceGeneric) -> str:
"""Извлекает имя соединения (GENERAL.CONNECTION) для устройства""" """Извлекает имя соединения (GENERAL.CONNECTION) для устройства"""
try: try:
applied_connection = await device.get_applied_connection() applied_connection = await device.get_applied_connection()
connection_settings = applied_connection[0] connection_settings = applied_connection[0]
connection_info = connection_settings.get('connection', {}) connection_info = connection_settings.get("connection", {})
return str(connection_info.get('id', ('s', 'unknown'))[1]) return str(connection_info.get("id", ("s", "unknown"))[1])
except Exception as e: except Exception as e:
# print(f"Ошибка при получении соединения для устройства {await device.interface}: {e}") # print(f"Ошибка при получении соединения для устройства {await device.interface}: {e}")
# срёт ошибки дл отключенных интерфейсов # срёт ошибки дл отключенных интерфейсов
# добавить потом условия для отключенных интерфейсов # добавить потом условия для отключенных интерфейсов
return 'unknown' return "unknown"
async def get_ip_config(self, device: NetworkDeviceGeneric, ip_version: str) -> List[IndividualIPConfig]: async def get_ip_config(self, device: NetworkDeviceGeneric,
ip_version: str) -> List[IndividualIPConfig]:
"""Получает конфигурацию IP для устройства""" """Получает конфигурацию IP для устройства"""
configs = [] configs = []
device_ip = await getattr(device, f'ip{ip_version}_config') device_ip = await getattr(device, f"ip{ip_version}_config")
if device_ip != '/': if device_ip != "/":
ip_config = globals()[f'IPv{ip_version}Config'](device_ip, self.system_bus) ip_config = globals()[f"IPv{ip_version}Config"](device_ip,
self.system_bus)
address_data = await ip_config.address_data address_data = await ip_config.address_data
for address in address_data: for address in address_data:
config = IndividualIPConfig( config = IndividualIPConfig(
address=str(address['address'][1]), address=str(address["address"][1]),
routes=[ routes=[
globals()[f'IPv{ip_version}Route']( globals()[f"IPv{ip_version}Route"](
destination=route.get('dest', ('s', '0.0.0.0' if ip_version == '4' else '::'))[1], destination=route.get(
prefix=route.get('prefix', ('u', 0))[1], "dest",
next_hop=route.get('next-hop', ('s', '0.0.0.0' if ip_version == '4' else '::'))[1], ("s",
metric=route.get('metric', ('u', 0))[1] "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 ) for route in await ip_config.route_data
], ],
method=await self.get_ip_method(device, f'ipv{ip_version}'), method=await self.get_ip_method(device,
f"ipv{ip_version}"),
gateway=str(await ip_config.gateway), gateway=str(await ip_config.gateway),
dns=[str(ns['address'][1]) if ip_version == '4' else str(ns) for ns in await getattr(ip_config, 'nameserver_data' if ip_version == '4' else 'nameservers')] dns=[
str(ns["address"][1]) if ip_version == "4" else str(ns)
for ns in await getattr(
ip_config,
"nameserver_data" if ip_version ==
"4" else "nameservers",
)
],
) )
configs.append(config) configs.append(config)
return configs return configs
async def get_wifi_info(self, wifi_device: NetworkDeviceWireless) -> Dict[str, Optional[Union[str, int]]]: async def get_wifi_info(
self, wifi_device: NetworkDeviceWireless
) -> Dict[str, Optional[Union[str, int]]]:
"""Получает информацию о Wi-Fi устройстве""" """Получает информацию о Wi-Fi устройстве"""
active_ap = await wifi_device.active_access_point active_ap = await wifi_device.active_access_point
wifi_info = { wifi_info = {
'ssid': None, "ssid": None,
'bssid': None, "bssid": None,
'bitrate': await wifi_device.bitrate, "bitrate": await wifi_device.bitrate,
'strength': None, "strength": None,
'frequency': None, "frequency": None,
'mode': None "mode": None,
} }
if active_ap != '/': if active_ap != "/":
ap = AccessPoint(active_ap, self.system_bus) ap = AccessPoint(active_ap, self.system_bus)
wifi_info.update({ wifi_info.update({
'ssid': await ap.ssid, "ssid": await ap.ssid,
'bssid': await ap.hw_address, "bssid": await ap.hw_address,
'strength': await ap.strength, "strength": await ap.strength,
'frequency': await ap.frequency, "frequency": await ap.frequency,
'mode': await ap.mode "mode": await ap.mode,
}) })
return wifi_info return wifi_info
async def get_network_interfaces(self, initial_positions: Dict[str, Tuple[int, int]] = None) -> List[Union[NetworkInterface, WiFiDevice]]: async def get_network_interfaces(
self,
initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[Union[NetworkInterface, WiFiDevice]]:
"""Получает список сетевых интерфейсов""" """Получает список сетевых интерфейсов"""
devices_paths = await self.nm.get_devices() devices_paths = await self.nm.get_devices()
network_interfaces = [] network_interfaces = []
@ -109,30 +133,44 @@ class NetworkService:
interface_name = str(await generic_device.interface) interface_name = str(await generic_device.interface)
device_type = DeviceType(await generic_device.device_type).name device_type = DeviceType(await generic_device.device_type).name
if device_type == 'LOOPBACK': if device_type == "LOOPBACK":
continue continue
device_state = str(await generic_device.state) device_state = str(await generic_device.state)
if device_state == '10': if device_state == "10":
continue continue
common_data = { common_data = {
'name': interface_name, "name":
'type': device_type, interface_name,
'device_state': device_state, "type":
'active_connection': await self.get_connection_name(generic_device), device_type,
'mac_address': str(await generic_device.hw_address), "device_state":
'device_mtu': str(await generic_device.mtu), device_state,
'ipv4': IPConfig(configurations=await self.get_ip_config(generic_device, '4')), "active_connection":
'ipv6': IPConfig(configurations=await self.get_ip_config(generic_device, '6')), await self.get_connection_name(generic_device),
'x': initial_positions.get(interface_name, (100, 100))[0], "mac_address":
'y': initial_positions.get(interface_name, (100, 100))[1], 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": if device_type == "WIFI":
wifi_device = NetworkDeviceWireless(device_path, self.system_bus) wifi_device = NetworkDeviceWireless(device_path,
self.system_bus)
wifi_info = await self.get_wifi_info(wifi_device) wifi_info = await self.get_wifi_info(wifi_device)
network_interfaces.append(WiFiDevice(**common_data, **wifi_info)) network_interfaces.append(
WiFiDevice(**common_data, **wifi_info))
else: else:
network_interfaces.append(NetworkInterface(**common_data)) network_interfaces.append(NetworkInterface(**common_data))
@ -141,7 +179,10 @@ class NetworkService:
async def get_hostname(self) -> Dict[str, str]: async def get_hostname(self) -> Dict[str, str]:
"""Возвращает имя хоста из команды hostname в формате JSON""" """Возвращает имя хоста из команды hostname в формате JSON"""
try: try:
result = subprocess.run(['hostname'], capture_output=True, text=True, check=True) result = subprocess.run(["hostname"],
capture_output=True,
text=True,
check=True)
hostname = result.stdout.strip() hostname = result.stdout.strip()
return {"hostname": hostname} return {"hostname": hostname}
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -149,52 +190,83 @@ class NetworkService:
except Exception as e: except Exception as e:
raise RuntimeError(f"Error processing hostname output: {e}") raise RuntimeError(f"Error processing hostname output: {e}")
async def get_interfaces_by_state(self, state: str, initial_positions: Dict[str, Tuple[int, int]] = None) -> List[NetworkInterface]: async def get_interfaces_by_state(
self,
state: str,
initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[NetworkInterface]:
""" """
Получает список интерфейсов по состоянию Получает список интерфейсов по состоянию
""" """
all_interfaces = await self.get_network_interfaces(initial_positions) all_interfaces = await self.get_network_interfaces(initial_positions)
return [interface for interface in all_interfaces if interface.device_state == state] return [
interface for interface in all_interfaces
if interface.device_state == state
]
async def get_interfaces_container(self, state: Optional[str] = None, initial_positions: Dict[str, Tuple[int, int]] = None, container_x: int = 0, container_y: int = 0) -> NetworkInterfacesContainer: async def get_interfaces_container(
self,
state: Optional[str] = None,
initial_positions: Dict[str, Tuple[int, int]] = None,
container_x: int = 0,
container_y: int = 0,
) -> NetworkInterfacesContainer:
"""Получает объект, содержащий список интерфейсов и координаты контейнера""" """Получает объект, содержащий список интерфейсов и координаты контейнера"""
if state: if state:
interfaces = await self.get_interfaces_by_state(state, initial_positions) interfaces = await self.get_interfaces_by_state(
state, initial_positions)
else: else:
interfaces = await self.get_network_interfaces(initial_positions) interfaces = await self.get_network_interfaces(initial_positions)
return NetworkInterfacesContainer( return NetworkInterfacesContainer(interfaces=interfaces,
interfaces=interfaces,
container_x=container_x, container_x=container_x,
container_y=container_y container_y=container_y)
)
async def get_network_interfaces_container(self, initial_positions: Optional[Dict[str, Tuple[int, int]]] = None, ) -> NetworkInterfacesContainer: async def get_network_interfaces_container(
self,
initial_positions: Optional[Dict[str, Tuple[int, int]]] = None,
) -> NetworkInterfacesContainer:
"""Получает контейнер с сетевыми интерфейсами и координатами контейнера""" """Получает контейнер с сетевыми интерфейсами и координатами контейнера"""
interfaces = await self.get_network_interfaces(initial_positions) interfaces = await self.get_network_interfaces(initial_positions)
return NetworkInterfacesContainer( return NetworkInterfacesContainer(interfaces=interfaces,
interfaces=interfaces,
container_x=0, container_x=0,
container_y=0 container_y=0)
)
async def get_activated_interfaces(self, initial_positions: Dict[str, Tuple[int, int]] = None) -> List[NetworkInterface]: async def get_activated_interfaces(
self,
initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[NetworkInterface]:
"""Получает список активных сетевых интерфейсов""" """Получает список активных сетевых интерфейсов"""
return await self.get_interfaces_by_state('100', initial_positions) return await self.get_interfaces_by_state("100", initial_positions)
async def get_deactivated_interfaces(self, initial_positions: Dict[str, Tuple[int, int]] = None) -> List[NetworkInterface]: async def get_deactivated_interfaces(
self,
initial_positions: Dict[str, Tuple[int, int]] = None
) -> List[NetworkInterface]:
"""Получает список неактивных сетевых интерфейсов""" """Получает список неактивных сетевых интерфейсов"""
all_interfaces = await self.get_network_interfaces(initial_positions) all_interfaces = await self.get_network_interfaces(initial_positions)
return [interface for interface in all_interfaces if interface.device_state != '100'] return [
interface for interface in all_interfaces
if interface.device_state != "100"
]
async def get_activated_interfaces_container(self, initial_positions: Dict[str, Tuple[int, int]] = None, container_x: int = 0, container_y: int = 0) -> NetworkInterfacesContainer: async def get_activated_interfaces_container(
self,
initial_positions: Dict[str, Tuple[int, int]] = None,
container_x: int = 0,
container_y: int = 0,
) -> NetworkInterfacesContainer:
"""Получает объект, содержащий список активных интерфейсов и координаты контейнера""" """Получает объект, содержащий список активных интерфейсов и координаты контейнера"""
return await self.get_interfaces_container('100', initial_positions, container_x, container_y) return await self.get_interfaces_container("100", initial_positions,
container_x, container_y)
async def get_deactivated_interfaces_container(self, initial_positions: Dict[str, Tuple[int, int]] = None, container_x: int = 0, container_y: int = 0) -> NetworkInterfacesContainer: async def get_deactivated_interfaces_container(
self,
initial_positions: Dict[str, Tuple[int, int]] = None,
container_x: int = 0,
container_y: int = 0,
) -> NetworkInterfacesContainer:
"""Получает объект, содержащий список неактивных интерфейсов и координаты контейнера""" """Получает объект, содержащий список неактивных интерфейсов и координаты контейнера"""
interfaces = await self.get_deactivated_interfaces(initial_positions) interfaces = await self.get_deactivated_interfaces(initial_positions)
return NetworkInterfacesContainer( return NetworkInterfacesContainer(interfaces=interfaces,
interfaces=interfaces,
container_x=container_x, container_x=container_x,
container_y=container_y container_y=container_y)
)

View file

@ -0,0 +1,424 @@
from scapy.config import conf
from mac_vendor_lookup import MacLookup
from typing import List, Optional, Tuple, Dict
import socket
import logging
import netifaces
import ipaddress
from concurrent.futures import ThreadPoolExecutor
import time
from functools import lru_cache
import json
from pathlib import Path
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import subprocess
from models.subnet import SubnetScanResult, Device
from config.ports import port_config
from scapy.layers.inet import IP, TCP
from scapy.layers.l2 import ARP, Ether
from scapy.sendrecv import srp, sr1
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
class NetworkScanner:
def __init__(self, interface: str):
self.interface = interface
conf.iface = interface
def scan(self, subnet: str) -> List[Tuple[str, str]]:
try:
arp = ARP(pdst=subnet)
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
packet = ether / arp
result = srp(packet,
timeout=2,
verbose=False,
iface=self.interface)[0]
return [(received.psrc, received.hwsrc)
for sent, received in result]
except Exception as e:
logger.error(f"Ошибка ARP-сканирования подсети {subnet}: {str(e)}")
return []
class PortScanner:
def __init__(self, interface: str):
self.interface = interface
def scan(self, ip: str) -> List[str]:
open_ports = []
def check_port(port: int) -> Optional[str]:
try:
pkt = sr1(
IP(dst=ip) / TCP(dport=port, flags="S"),
timeout=2,
verbose=False,
)
if pkt and pkt.haslayer(TCP) and pkt[TCP].flags == 0x12:
return port_config.get_port_name(port)
return None
except Exception as e:
logger.debug(
f"Ошибка проверки порта {port} для IP {ip}: {str(e)}")
return None
try:
max_workers = min(len(port_config.get_ports()), 15)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
results = executor.map(check_port, port_config.get_ports())
open_ports = [port for port in results if port is not None]
logger.debug(f"Открытые порты для IP {ip}: {open_ports}")
return open_ports
except Exception as e:
logger.warning(
f"Ошибка при сканировании портов для IP {ip}: {str(e)}")
return []
class DeviceInfoCollector:
def __init__(self):
self.mac_lookup = MacLookup()
self.nbtscan_cache = {}
self.nbtscan_results = {}
@lru_cache(maxsize=1000)
def get_vendor(self, mac: str) -> str:
try:
return self.mac_lookup.lookup(mac)
except Exception as e:
logger.warning(
f"Не удалось определить производителя для MAC {mac}: {str(e)}")
return "Unknown"
def scan_nbtscan(self, subnet: str) -> None:
if subnet not in self.nbtscan_results:
try:
logger.debug(f"Запуск nbtscan для подсети {subnet}")
result = subprocess.run(["sudo", "nbtscan", "-r", str(subnet)],
capture_output=True,
text=True,
check=False,
timeout=10)
logger.debug(f"Полный вывод nbtscan: {result.stdout}")
if result.returncode == 0:
for line in result.stdout.splitlines():
logger.debug(f"Обработка строки: {line}")
if not line.startswith(("IP address", "-", "Doing NBT")):
parts = [p for p in line.split() if p]
if len(parts) >= 2:
ip_addr, hostname = parts[0], parts[1]
if hostname != "<unknown>":
self.nbtscan_cache[ip_addr] = hostname
logger.info(f"NetBIOS: Найден хостнейм {hostname} для IP {ip_addr}")
self.nbtscan_results[subnet] = True
except subprocess.TimeoutExpired:
logger.error(f"NetBIOS: Таймаут для подсети {subnet}")
except Exception as e:
logger.error(f"NetBIOS: Ошибка для подсети {subnet}: {str(e)}")
def get_hostname(self, ip: str) -> Optional[str]:
try:
socket.setdefaulttimeout(2)
return socket.gethostbyaddr(ip)[0]
except (socket.herror, socket.gaierror, socket.timeout):
logger.debug(f"DNS: Не удалось разрешить имя для IP {ip}")
subnet = ipaddress.ip_network(f"{ip}/24", strict=False)
if subnet not in self.nbtscan_results:
self.scan_nbtscan(str(subnet))
if ip in self.nbtscan_cache:
return self.nbtscan_cache[ip]
logger.debug(f"Не удалось определить хостнейм для IP {ip} через DNS или NetBIOS")
return None
class DeviceStorage:
def __init__(self, storage_path: str = "etc/gonets/SRV/storage/devices/devices.json"):
self.storage_file = Path(storage_path)
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
self.offline_timeout = timedelta(days=7)
def load_devices(self) -> Dict[str, Device]:
try:
if self.storage_file.exists():
with open(self.storage_file, "r", encoding="utf-8") as f:
data = json.load(f)
devices = {}
for device_data in data.get("devices", []):
device = Device(**device_data)
if device.mac:
devices[device.mac] = device
return self._clean_old_devices(devices)
return {}
except Exception as e:
logger.error(f"Ошибка при загрузке устройств: {str(e)}")
return {}
def _clean_old_devices(self, devices: Dict[str, Device]) -> Dict[str, Device]:
now = datetime.now()
filtered = {}
for mac, device in devices.items():
if device.status == "offline" and device.last_seen:
try:
last_seen = datetime.fromisoformat(device.last_seen)
if now - last_seen <= self.offline_timeout:
filtered[mac] = device
else:
logger.info(f"Удалено устройство {mac} (offline > 7 дней)")
except ValueError:
logger.warning(f"Неверный формат last_seen для устройства {mac}")
else:
filtered[mac] = device
return filtered
def save_devices(self, devices: List[Device]) -> None:
try:
data = SubnetScanResult(devices=devices).model_dump()
with open(self.storage_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
logger.info(f"Сохранено {len(devices)} устройств в {self.storage_file}")
except Exception as e:
logger.error(f"Ошибка при сохранении устройств: {str(e)}")
class SubnetService:
def __init__(self):
self.interface = self._get_default_interface()
self.network_scanner = NetworkScanner(self.interface)
self.port_scanner = PortScanner(self.interface)
self.device_info = DeviceInfoCollector()
self.storage = DeviceStorage()
def _get_default_interface(self) -> str:
try:
gateways = netifaces.gateways()
default_gateway = gateways.get("default", {}).get(netifaces.AF_INET)
if default_gateway:
return default_gateway[1]
interfaces = netifaces.interfaces()
for iface in interfaces:
if iface != "lo":
return iface
return "eth0"
except Exception as e:
logger.warning(f"Не удалось определить интерфейс: {str(e)}")
return "eth0"
def scan_subnet(self, subnet: str, ports: List[int] = None) -> SubnetScanResult:
start_time = time.time()
devices = []
now = datetime.now().astimezone()
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
try:
saved_devices = self.storage.load_devices()
local_device = self._get_local_device(subnet)
if local_device:
local_device.status = "online"
local_device.last_seen = formatted_now
devices.append(local_device)
if local_device.mac:
saved_devices[local_device.mac] = local_device
discovered_devices = self.network_scanner.scan(subnet)
self.device_info.scan_nbtscan(subnet)
def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]:
ip, mac = ip_mac
if local_device and ip == local_device.ip:
return None
existing_device = saved_devices.get(mac)
if existing_device:
return Device(
ip=ip,
mac=mac,
hostname=self.device_info.get_hostname(ip) or existing_device.hostname,
vendor=self.device_info.get_vendor(mac),
open_ports=self.port_scanner.scan(ip) or existing_device.open_ports,
status="online",
last_seen=formatted_now,
note=existing_device.note,
)
vendor = self.device_info.get_vendor(mac)
hostname = self.device_info.get_hostname(ip)
open_ports = self.port_scanner.scan(ip)
return Device(
ip=ip,
mac=mac,
hostname=hostname,
vendor=vendor,
open_ports=open_ports,
status="online",
last_seen=formatted_now,
note=None,
)
with ThreadPoolExecutor(max_workers=10) as executor:
device_results = executor.map(process_device, discovered_devices)
devices.extend([d for d in device_results if d is not None])
active_macs = {d.mac for d in devices if d.mac}
for mac, device in saved_devices.items():
if mac not in active_macs:
device.status = "offline"
devices.append(device)
devices.sort(key=lambda x: int(ipaddress.IPv4Address(x.ip)))
for device in devices:
if device.mac:
saved_devices[device.mac] = device
self.storage.save_devices(list(saved_devices.values()))
logger.info(
f"Сканирование подсети {subnet} завершено за {time.time() - start_time:.2f} секунд, "
f"найдено {len(devices)} устройств (активных: {len(active_macs)})"
)
return SubnetScanResult(devices=devices)
except Exception as e:
logger.error(f"Ошибка при сканировании подсети {subnet}: {str(e)}")
return SubnetScanResult(devices=devices)
def _get_local_device(self, subnet: str) -> Optional[Device]:
now = datetime.now().astimezone()
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
try:
subnet_network = ipaddress.ip_network(subnet, strict=False)
interfaces = netifaces.interfaces()
for iface in interfaces:
if iface == "lo":
continue
addrs = netifaces.ifaddresses(iface)
if netifaces.AF_INET in addrs and netifaces.AF_LINK in addrs:
ip = addrs[netifaces.AF_INET][0]["addr"]
if ipaddress.ip_address(ip) in subnet_network:
mac = addrs[netifaces.AF_LINK][0]["addr"]
vendor = self.device_info.get_vendor(mac)
hostname = self.device_info.get_hostname(ip)
open_ports = self.port_scanner.scan(ip)
logger.debug(
f"Локальное устройство: IP={ip}, MAC={mac}, Ports={open_ports}, Hostname={hostname}"
)
return Device(
ip=ip,
mac=mac,
hostname=hostname,
vendor=vendor,
open_ports=open_ports,
status="online",
last_seen=formatted_now,
note=None,
)
logger.warning(
f"Локальное устройство не найдено в подсети {subnet}")
return None
except Exception as e:
logger.warning(
f"Не удалось получить локальное устройство: {str(e)}")
return None
def add_note(self, mac: str, note: str) -> Tuple[bool, Optional[str]]:
"""Добавляет заметку к устройству по MAC-адресу.
Args:
mac (str): MAC-адрес устройства.
note (str): Текст заметки.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_devices = self.storage.load_devices()
if mac not in saved_devices:
logger.warning(f"Устройство с MAC {mac} не найдено")
return False, f"Устройство с MAC {mac} не найдено"
if saved_devices[mac].note is not None:
logger.error(
f"Заметка для устройства с MAC {mac} уже существует")
return False, f"Заметка для устройства с MAC {mac} уже существует"
saved_devices[mac].note = note
self.storage.save_devices(list(saved_devices.values()))
logger.info(
f"Добавлена заметка '{note}' для устройства с MAC {mac}")
return True, None
except Exception as e:
logger.error(
f"Ошибка при добавлении заметки для MAC {mac}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"
def update_note(self, mac: str, note: str) -> Tuple[bool, Optional[str]]:
"""Обновляет заметку устройства по MAC-адресу.
Args:
mac (str): MAC-адрес устройства.
note (str): Новый текст заметки.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_devices = self.storage.load_devices()
if mac not in saved_devices:
logger.warning(f"Устройство с MAC {mac} не найдено")
return False, f"Устройство с MAC {mac} не найдено"
if saved_devices[mac].note is None:
logger.error(
f"Заметка для устройства с MAC {mac} не существует")
return False, f"Заметка для устройства с MAC {mac} не существует"
saved_devices[mac].note = note
self.storage.save_devices(list(saved_devices.values()))
logger.info(
f"Обновлена заметка '{note}' для устройства с MAC {mac}")
return True, None
except Exception as e:
logger.error(
f"Ошибка при обновлении заметки для MAC {mac}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"
def delete_note(self, mac: str) -> Tuple[bool, Optional[str]]:
"""Удаляет заметку устройства по MAC-адресу.
Args:
mac (str): MAC-адрес устройства.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_devices = self.storage.load_devices()
if mac not in saved_devices:
logger.warning(f"Устройство с MAC {mac} не найдено")
return False, f"Устройство с MAC {mac} не найдено"
if saved_devices[mac].note is None:
logger.error(
f"Заметка для устройства с MAC {mac} не существует")
return False, f"Заметка для устройства с MAC {mac} не существует"
saved_devices[mac].note = None
self.storage.save_devices(list(saved_devices.values()))
logger.info(f"Удалена заметка для устройства с MAC {mac}")
return True, None
except Exception as e:
logger.error(
f"Ошибка при удалении заметки для MAC {mac}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"

View file

@ -41,9 +41,9 @@ class UFWService:
print(f"Command error: {result.stderr}") print(f"Command error: {result.stderr}")
if result.returncode != 0: if result.returncode != 0:
raise subprocess.CalledProcessError( raise subprocess.CalledProcessError(result.returncode, command,
result.returncode, command, result.stdout, result.stderr result.stdout,
) result.stderr)
return result.stdout return result.stdout
@ -116,9 +116,8 @@ class UFWService:
""" """
try: try:
status_result = self._run_command("ufw status") status_result = self._run_command("ufw status")
first_line = ( first_line = (status_result.splitlines()[0]
status_result.splitlines()[0] if status_result.splitlines() else "" if status_result.splitlines() else "")
)
is_enabled = "Status: active" in first_line is_enabled = "Status: active" in first_line
message = "UFW активен" if is_enabled else "UFW неактивен" message = "UFW активен" if is_enabled else "UFW неактивен"
@ -166,7 +165,9 @@ class UFWService:
print(f"Ошибка при выключении UFW: {e}") print(f"Ошибка при выключении UFW: {e}")
return False return False
def add_rules_base(self, rules: List[UFWRule], direction: str = "") -> bool: def add_rules_base(self,
rules: List[UFWRule],
direction: str = "") -> bool:
""" """
Базовый метод для добавления правил UFW. Базовый метод для добавления правил UFW.
@ -296,7 +297,10 @@ class UFWService:
except Exception as e: except Exception as e:
error_msg = f"Ошибка выполнения команды: {str(e)}" error_msg = f"Ошибка выполнения команды: {str(e)}"
print(error_msg) print(error_msg)
results.append({"success": False, "message": error_msg}) results.append({
"success": False,
"message": error_msg
})
except Exception as e: except Exception as e:
error_msg = f"Ошибка при добавлении правила: {str(e)}" error_msg = f"Ошибка при добавлении правила: {str(e)}"
@ -349,32 +353,37 @@ class UFWService:
try: try:
# Проверяем существует ли правило с таким номером # Проверяем существует ли правило с таким номером
if rule_number not in rules: if rule_number not in rules:
results.append( results.append({
{ "number":
"number": rule_number, rule_number,
"success": False, "success":
"error": f"Правило с номером {rule_number} не найдено", False,
} "error":
) f"Правило с номером {rule_number} не найдено",
})
continue continue
# Удаляем правило # Удаляем правило
self._run_command(f"ufw --force delete {rule_number}") self._run_command(f"ufw --force delete {rule_number}")
results.append( results.append({
{ "number":
"number": rule_number, rule_number,
"success": True, "success":
"message": f"Правило {rule_number} успешно удалено", True,
} "message":
) f"Правило {rule_number} успешно удалено",
})
except Exception as e: except Exception as e:
if DEBUG: if DEBUG:
print(f"Ошибка при удалении правила {rule_number}: {e}") print(
results.append( f"Ошибка при удалении правила {rule_number}: {e}")
{"number": rule_number, "success": False, "error": str(e)} results.append({
) "number": rule_number,
"success": False,
"error": str(e)
})
# Подсчитываем статистику # Подсчитываем статистику
total = len(rule_numbers) total = len(rule_numbers)

View file

@ -0,0 +1,392 @@
import time
import logging
import subprocess
import re
from functools import lru_cache
from typing import List, Optional, Dict, Tuple
from mac_vendor_lookup import MacLookup
import pywifi
from pywifi import Profile
from models.wifi import WifiAccessPoint, WifiScanResult
from pathlib import Path
import json
from datetime import datetime
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
class WifiStorage:
"""Класс для хранения данных о Wi-Fi сетях и их заметках по SSID."""
def __init__(self, storage_path: str = "etc/gonets/SRV/storage/wifi/wifi.json"):
self.storage_file = Path(storage_path)
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
def load_notes(self) -> Dict[str, Optional[str]]:
"""Загружает заметки по SSID из файла хранения."""
try:
if self.storage_file.exists():
with open(self.storage_file, "r", encoding="utf-8") as f:
data = json.load(f)
return {item["ssid"]: item.get("note") for item in data.get("networks", [])}
return {}
except Exception as e:
logger.error(f"Ошибка при загрузке заметок: {str(e)}")
return {}
def save_notes(self, notes: Dict[str, Optional[str]]) -> None:
"""Сохраняет заметки по SSID в файл."""
try:
data = {"networks": [{"ssid": ssid, "note": note} for ssid, note in notes.items()]}
with open(self.storage_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
logger.info(f"Сохранено {len(notes)} заметок в {self.storage_file}")
except Exception as e:
logger.error(f"Ошибка при сохранении заметок: {str(e)}")
class WifiService:
"""Сервис для сканирования текущих Wi-Fi точек доступа"""
def __init__(self):
self.mac_lookup = MacLookup()
self.wifi = pywifi.PyWiFi()
self.interface = self._get_wifi_interface()
self.storage = WifiStorage()
def _get_wifi_interface(self) -> Optional[object]:
"""Автоматически определяет активный Wi-Fi-интерфейс"""
try:
interfaces = self.wifi.interfaces()
if not interfaces:
logger.error("Не найдено сетевых интерфейсов")
return None
logger.debug(
f"Доступные интерфейсы: {[iface.name() for iface in interfaces]}"
)
for iface in interfaces:
iface_name = iface.name()
if "p2p" in iface_name.lower():
logger.debug(f"Пропущен интерфейс {iface_name} (P2P)")
continue
try:
iface.scan()
time.sleep(1)
results = iface.scan_results()
if results:
logger.info(f"Выбран Wi-Fi-интерфейс: {iface_name}")
return iface
logger.debug(
f"Интерфейс {iface_name} не вернул результатов сканирования"
)
except Exception as e:
logger.debug(f"Интерфейс {iface_name} неактивен: {str(e)}")
logger.error("Не найден активный Wi-Fi-интерфейс")
return None
except Exception as e:
logger.error(f"Ошибка при определении Wi-Fi-интерфейса: {str(e)}")
return None
@lru_cache(maxsize=1000)
def get_vendor(self, mac: str) -> str:
try:
return self.mac_lookup.lookup(mac)
except Exception as e:
logger.warning(
f"Не удалось определить производителя для MAC {mac}: {str(e)}")
return "Unknown"
def _get_encryption_from_iw(self, ap_info: Dict) -> str:
"""Определяет шифрование из данных iw"""
security = ap_info.get("security", "")
logger.debug(
f"Шифрование для BSSID {ap_info.get('bssid')}: {security}")
if "WPA3" in security:
return "WPA3"
elif "WPA2" in security or "RSN" in security:
return "WPA2"
elif "WPA" in security:
return "WPA"
elif "WEP" in security:
return "WEP"
return "Open"
def _get_channel(self, frequency: Optional[int]) -> Optional[int]:
"""Преобразует частоту в канал."""
if frequency is None:
return None
try:
if 2412 <= frequency <= 2472:
return (frequency - 2412) // 5 + 1
elif 5180 <= frequency <= 5825:
return (frequency - 5180) // 5 + 36
logger.debug(f"Частота {frequency} вне диапазона 2.4/5 GHz")
return None
except Exception as e:
logger.debug(
f"Ошибка при определении канала для частоты {frequency}: {str(e)}"
)
return None
def _get_frequency(self, frequency: Optional[int]) -> Optional[str]:
"""Определяет частотный диапазон."""
if frequency is None:
return None
try:
if 2412 <= frequency <= 2472:
return "2.4 GHz"
elif 5180 <= frequency <= 5825:
return "5 GHz"
logger.debug(f"Частота {frequency} вне диапазона 2.4/5 GHz")
return None
except Exception as e:
logger.debug(
f"Ошибка при определении диапазона для частоты {frequency}: {str(e)}"
)
return None
def _get_iw_scan_data(self) -> List[Dict]:
"""Получает данные сканирования через iw."""
try:
if not self.interface:
logger.error("Интерфейс не определён")
return []
cmd = ["sudo", "iw", "dev", self.interface.name(), "scan"]
logger.debug(f"Выполняем команду: {' '.join(cmd)}")
result = subprocess.run(cmd,
capture_output=True,
text=True,
timeout=10)
if result.returncode != 0:
logger.error(f"Ошибка iw scan: {result.stderr}")
return []
output = result.stdout
logger.debug(
f"Вывод iw scan (первые 500 символов): {output[:500]}...")
aps = []
current_ap = None
bssid_pattern = re.compile(r"BSS ([0-9a-f:]{17})")
ssid_pattern = re.compile(r"SSID: ?(.*)")
freq_pattern = re.compile(r"freq: (\d+)")
signal_pattern = re.compile(r"signal: ([-\d.]+) dBm")
rsn_pattern = re.compile(r"RSN:")
wpa_pattern = re.compile(r"WPA:")
security_type_pattern = re.compile(r"\* (WPA3|WPA2|WPA|WEP)")
for line in output.splitlines():
line = line.strip()
if bssid_match := bssid_pattern.match(line):
if current_ap:
aps.append(current_ap)
current_ap = {
"bssid": bssid_match.group(1).lower(),
"security": ""
}
elif current_ap:
if ssid_match := ssid_pattern.match(line):
current_ap["ssid"] = (ssid_match.group(1)
if ssid_match.group(1).strip()
else "")
elif freq_match := freq_pattern.match(line):
current_ap["frequency"] = int(freq_match.group(1))
elif signal_match := signal_pattern.match(line):
current_ap["signal"] = float(signal_match.group(1))
elif rsn_pattern.match(line):
current_ap["security"] += "RSN;"
elif wpa_pattern.match(line):
current_ap["security"] += "WPA;"
elif security_type_match := security_type_pattern.search(
line):
current_ap["security"] += security_type_match.group(
1) + ";"
if current_ap:
aps.append(current_ap)
logger.debug(
f"Найдено {len(aps)} точек через iw: {[ap['bssid'] for ap in aps]}"
)
for ap in aps:
logger.debug(f"Данные точки {ap['bssid']}: {ap}")
return aps
except subprocess.CalledProcessError as e:
logger.error(f"Ошибка при выполнении iw scan: {e.stderr}")
return []
except subprocess.TimeoutExpired:
logger.error("Таймаут при выполнении iw scan")
return []
except Exception as e:
logger.error(f"Ошибка при парсинге iw scan: {str(e)}")
return []
def scan_wifi(self, timeout: int = 30) -> WifiScanResult:
"""Сканирует текущие Wi-Fi точки доступа"""
start_time = time.time()
aps = []
if not self.interface:
logger.error("Wi-Fi-интерфейс не доступен")
return WifiScanResult(access_points=aps)
try:
logger.info(
f"Начало сканирования Wi-Fi на интерфейсе {self.interface.name()}"
)
self.interface.scan()
time.sleep(10)
py_results = self.interface.scan_results()
logger.debug(f"Найдено {len(py_results)} точек через pywifi")
iw_results = self._get_iw_scan_data()
iw_by_bssid = {ap["bssid"]: ap for ap in iw_results if ap.get("bssid")}
best_aps_by_ssid: Dict[str, WifiAccessPoint] = {}
saved_notes = self.storage.load_notes()
for ap in py_results:
bssid = ap.bssid.lower() if ap.bssid else None
if not bssid:
logger.debug("Пропущена точка без BSSID")
continue
ssid = ap.ssid if ap.ssid else ""
hidden = not ssid
signal = ap.signal if ap.signal else None
iw_data = iw_by_bssid.get(bssid, {})
frequency = iw_data.get("frequency")
channel = self._get_channel(frequency)
freq_band = self._get_frequency(frequency)
encryption = self._get_encryption_from_iw(iw_data)
vendor = self.get_vendor(bssid)
logger.debug(
f"Точка {bssid}: freq={frequency}, channel={channel}, encryption={encryption}, signal={signal}"
)
current_ap = WifiAccessPoint(
ssid=ssid,
bssid=bssid,
channel=channel,
signal=signal if signal else iw_data.get("signal"),
encryption=encryption,
frequency=freq_band,
vendor=vendor,
hidden=hidden,
)
if ssid not in best_aps_by_ssid or (
current_ap.signal and best_aps_by_ssid[ssid].signal
and current_ap.signal > best_aps_by_ssid[ssid].signal):
# При обновлении или добавлении точки сохраняем заметку из saved_notes
current_ap.note = saved_notes.get(ssid)
best_aps_by_ssid[ssid] = current_ap
aps = list(best_aps_by_ssid.values())
# Сохраняем заметки для всех найденных SSID
notes_to_save = {ap.ssid: ap.note for ap in aps if ap.ssid}
self.storage.save_notes(notes_to_save)
logger.info(
f"Сканирование Wi-Fi завершено за {time.time() - start_time:.2f} секунд, "
f"найдено {len(aps)} уникальных сетей")
return WifiScanResult(access_points=aps)
except Exception as e:
logger.error(f"Ошибка при сканировании Wi-Fi: {str(e)}")
return WifiScanResult(access_points=aps)
def add_note(self, ssid: str, note: str) -> Tuple[bool, Optional[str]]:
"""Добавляет заметку к сети Wi-Fi по SSID.
Args:
ssid (str): SSID сети Wi-Fi.
note (str): Текст заметки.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_notes = self.storage.load_notes()
if ssid not in saved_notes or saved_notes[ssid] is None:
logger.info(f"Добавлена новая заметка для сети с SSID {ssid}")
elif saved_notes[ssid] is not None:
logger.error(f"Заметка для сети с SSID {ssid} уже существует")
return False, f"Заметка для сети с SSID {ssid} уже существует"
saved_notes[ssid] = note
self.storage.save_notes(saved_notes)
logger.info(f"Добавлена заметка '{note}' для сети с SSID {ssid}")
return True, None
except Exception as e:
logger.error(f"Ошибка при добавлении заметки для SSID {ssid}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"
def update_note(self, ssid: str, note: str) -> Tuple[bool, Optional[str]]:
"""Обновляет заметку сети Wi-Fi по SSID.
Args:
ssid (str): SSID сети Wi-Fi.
note (str): Новый текст заметки.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_notes = self.storage.load_notes()
if ssid not in saved_notes:
logger.warning(f"Сеть с SSID {ssid} не найдена")
return False, f"Сеть с SSID {ssid} не найдена"
if saved_notes[ssid] is None:
logger.error(f"Заметка для сети с SSID {ssid} не существует")
return False, f"Заметка для сети с SSID {ssid} не существует"
saved_notes[ssid] = note
self.storage.save_notes(saved_notes)
logger.info(f"Обновлена заметка '{note}' для сети с SSID {ssid}")
return True, None
except Exception as e:
logger.error(f"Ошибка при обновлении заметки для SSID {ssid}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"
def delete_note(self, ssid: str) -> Tuple[bool, Optional[str]]:
"""Удаляет заметку сети Wi-Fi по SSID.
Args:
ssid (str): SSID сети Wi-Fi.
Returns:
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
- Успех: (True, None).
- Ошибка: (False, сообщение об ошибке).
Raises:
Exception: Если произошла ошибка при сохранении.
"""
try:
saved_notes = self.storage.load_notes()
if ssid not in saved_notes:
logger.warning(f"Сеть с SSID {ssid} не найдена")
return False, f"Сеть с SSID {ssid} не найдена"
if saved_notes[ssid] is None:
logger.error(f"Заметка для сети с SSID {ssid} не существует")
return False, f"Заметка для сети с SSID {ssid} не существует"
saved_notes[ssid] = None
self.storage.save_notes(saved_notes)
logger.info(f"Удалена заметка для сети с SSID {ssid}")
return True, None
except Exception as e:
logger.error(f"Ошибка при удалении заметки для SSID {ssid}: {str(e)}")
return False, f"Внутренняя ошибка сервера: {str(e)}"

View file

@ -9,6 +9,7 @@ from models.auth import UserCreate, UserResponse
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class JsonStorage: class JsonStorage:
"""Хранилище пользователей в JSON-файле""" """Хранилище пользователей в JSON-файле"""
@ -22,7 +23,8 @@ class JsonStorage:
try: try:
logger.info(f"Проверка существования файла {self.file_path}") logger.info(f"Проверка существования файла {self.file_path}")
if not os.path.exists(self.file_path): if not os.path.exists(self.file_path):
logger.info(f"Создание директории {os.path.dirname(self.file_path)}") logger.info(
f"Создание директории {os.path.dirname(self.file_path)}")
os.makedirs(os.path.dirname(self.file_path), exist_ok=True) os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
logger.info(f"Создание файла {self.file_path}") logger.info(f"Создание файла {self.file_path}")
with open(self.file_path, "w") as f: with open(self.file_path, "w") as f:
@ -32,7 +34,9 @@ class JsonStorage:
else: else:
logger.info(f"Файл {self.file_path} уже существует") logger.info(f"Файл {self.file_path} уже существует")
except Exception as e: except Exception as e:
logger.error(f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}"
)
raise raise
def _read_data(self) -> dict: def _read_data(self) -> dict:
@ -41,10 +45,14 @@ class JsonStorage:
with open(self.file_path, "r") as f: with open(self.file_path, "r") as f:
return json.load(f) return json.load(f)
except FileNotFoundError: except FileNotFoundError:
logger.info(f"Файл {self.file_path} не существует, возвращаем пустую структуру") logger.info(
f"Файл {self.file_path} не существует, возвращаем пустую структуру"
)
return {"users": [], "next_id": 1} return {"users": [], "next_id": 1}
except Exception as e: except Exception as e:
logger.error(f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}"
)
raise raise
def _write_data(self, data: dict): def _write_data(self, data: dict):
@ -53,7 +61,9 @@ class JsonStorage:
with open(self.file_path, "w") as f: with open(self.file_path, "w") as f:
json.dump(data, f, indent=2) json.dump(data, f, indent=2)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}"
)
raise raise
def create_user(self, user: UserCreate) -> UserResponse: def create_user(self, user: UserCreate) -> UserResponse:
@ -70,22 +80,31 @@ class JsonStorage:
"id": next_id, "id": next_id,
"username": user.username, "username": user.username,
"password_hash": password_hash, "password_hash": password_hash,
"is_admin": user.is_admin "is_admin": user.is_admin,
} }
users.append(new_user) users.append(new_user)
data["next_id"] = next_id + 1 data["next_id"] = next_id + 1
self._write_data(data) self._write_data(data)
logger.info(f"Пользователь {user.username} успешно зарегистрирован") logger.info(
return UserResponse(id=new_user["id"], username=new_user["username"], is_admin=new_user["is_admin"]) f"Пользователь {user.username} успешно зарегистрирован")
return UserResponse(
id=new_user["id"],
username=new_user["username"],
is_admin=new_user["is_admin"],
)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при создании пользователя {user.username}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при создании пользователя {user.username}: {type(e).__name__} - {str(e)}"
)
raise raise
def get_user_by_username(self, username: str) -> Optional[dict]: def get_user_by_username(self, username: str) -> Optional[dict]:
"""Получает пользователя по имени""" """Получает пользователя по имени"""
try: try:
if not os.path.exists(self.file_path): if not os.path.exists(self.file_path):
logger.info(f"Файл {self.file_path} не существует, пользователь {username} не найден") logger.info(
f"Файл {self.file_path} не существует, пользователь {username} не найден"
)
return None return None
data = self._read_data() data = self._read_data()
for user in data["users"]: for user in data["users"]:
@ -93,9 +112,12 @@ class JsonStorage:
return user return user
return None return None
except Exception as e: except Exception as e:
logger.error(f"Ошибка при получении пользователя {username}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при получении пользователя {username}: {type(e).__name__} - {str(e)}"
)
raise raise
def verify_password(self, plain_password: str, hashed_password: str) -> bool: def verify_password(self, plain_password: str,
hashed_password: str) -> bool:
"""Проверяет пароль""" """Проверяет пароль"""
return self.pwd_context.verify(plain_password, hashed_password) return self.pwd_context.verify(plain_password, hashed_password)

View file

@ -9,7 +9,9 @@ from datetime import datetime, timedelta
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SessionStorage: class SessionStorage:
def __init__(self, file_path: str = "/etc/gonets/sessions.json"): def __init__(self, file_path: str = "/etc/gonets/sessions.json"):
self.file_path = file_path self.file_path = file_path
self._ensure_file_exists() self._ensure_file_exists()
@ -18,7 +20,8 @@ class SessionStorage:
try: try:
logger.info(f"Проверка существования файла {self.file_path}") logger.info(f"Проверка существования файла {self.file_path}")
if not os.path.exists(self.file_path): if not os.path.exists(self.file_path):
logger.info(f"Создание директории {os.path.dirname(self.file_path)}") logger.info(
f"Создание директории {os.path.dirname(self.file_path)}")
os.makedirs(os.path.dirname(self.file_path), exist_ok=True) os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
logger.info(f"Создание файла {self.file_path}") logger.info(f"Создание файла {self.file_path}")
with open(self.file_path, "w") as f: with open(self.file_path, "w") as f:
@ -28,7 +31,9 @@ class SessionStorage:
else: else:
logger.info(f"Файл {self.file_path} уже существует") logger.info(f"Файл {self.file_path} уже существует")
except Exception as e: except Exception as e:
logger.error(f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}"
)
raise raise
def _read_data(self) -> Dict: def _read_data(self) -> Dict:
@ -36,7 +41,9 @@ class SessionStorage:
with open(self.file_path, "r") as f: with open(self.file_path, "r") as f:
return json.load(f) return json.load(f)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}"
)
raise raise
def _write_data(self, data: Dict): def _write_data(self, data: Dict):
@ -44,7 +51,9 @@ class SessionStorage:
with open(self.file_path, "w") as f: with open(self.file_path, "w") as f:
json.dump(data, f, indent=2) json.dump(data, f, indent=2)
except Exception as e: except Exception as e:
logger.error(f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}"
)
raise raise
def create_session(self, username: str, token: str, client_id: str) -> str: def create_session(self, username: str, token: str, client_id: str) -> str:
@ -60,13 +69,17 @@ class SessionStorage:
"token": token, "token": token,
"client_id": client_id, "client_id": client_id,
"created_at": created_at, "created_at": created_at,
"expiry": expiry "expiry": expiry,
} }
self._write_data(data) self._write_data(data)
logger.info(f"Сессия создана для пользователя {username} с session_id {session_id}") logger.info(
f"Сессия создана для пользователя {username} с session_id {session_id}"
)
return session_id return session_id
except Exception as e: except Exception as e:
logger.error(f"Ошибка при создании сессии для {username}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при создании сессии для {username}: {type(e).__name__} - {str(e)}"
)
raise raise
def get_latest_session(self, client_id: str) -> Optional[Dict]: def get_latest_session(self, client_id: str) -> Optional[Dict]:
@ -95,7 +108,9 @@ class SessionStorage:
self._write_data(data) self._write_data(data)
return None return None
except Exception as e: except Exception as e:
logger.error(f"Ошибка при получении сессии для client_id {client_id}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при получении сессии для client_id {client_id}: {type(e).__name__} - {str(e)}"
)
raise raise
def delete_session(self, session_id: str): def delete_session(self, session_id: str):
@ -108,7 +123,9 @@ class SessionStorage:
self._write_data(data) self._write_data(data)
logger.info(f"Сессия с session_id {session_id} удалена") logger.info(f"Сессия с session_id {session_id} удалена")
except Exception as e: except Exception as e:
logger.error(f"Ошибка при удалении сессии {session_id}: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при удалении сессии {session_id}: {type(e).__name__} - {str(e)}"
)
raise raise
def clean_expired_sessions(self): def clean_expired_sessions(self):
@ -127,7 +144,8 @@ class SessionStorage:
# Удаляем истекшие сессии # Удаляем истекшие сессии
for session_id in expired_sessions: for session_id in expired_sessions:
del data["sessions"][session_id] del data["sessions"][session_id]
logger.info(f"Удалена истекшая сессия с session_id {session_id}") logger.info(
f"Удалена истекшая сессия с session_id {session_id}")
# Записываем обновлённые данные # Записываем обновлённые данные
if expired_sessions: if expired_sessions:
@ -137,5 +155,7 @@ class SessionStorage:
logger.debug("Истекшие сессии не найдены") logger.debug("Истекшие сессии не найдены")
except Exception as e: except Exception as e:
logger.error(f"Ошибка при очистке истекших сессий: {type(e).__name__} - {str(e)}") logger.error(
f"Ошибка при очистке истекших сессий: {type(e).__name__} - {str(e)}"
)
raise raise

View file

@ -1,5 +1,5 @@
gonets (0.2-5) UNRELEASED; urgency=medium gonets (0.3-1) UNRELEASED; urgency=medium
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP> * Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
-- GOSSYS LLC <devteam@gossys.dev> Thu, 08 May 2025 15:39:41 +0500 -- GOSSYS LLC <devteam@gossys.dev> Thu, 19 Jun 2025 13:24:42 +0500

View file

@ -1,11 +1,11 @@
Package: gonets-dbgsym Package: gonets-dbgsym
Source: gonets Source: gonets
Version: 0.2-5 Version: 0.3-1
Auto-Built-Package: debug-symbols Auto-Built-Package: debug-symbols
Architecture: amd64 Architecture: amd64
Maintainer: GOSSYS LLC <devteam@gossys.dev> Maintainer: GOSSYS LLC <devteam@gossys.dev>
Installed-Size: 1528 Installed-Size: 1528
Depends: gonets (= 0.2-5) Depends: gonets (= 0.3-1)
Section: debug Section: debug
Priority: optional Priority: optional
Description: debug symbols for gonets Description: debug symbols for gonets

View file

@ -1,6 +0,0 @@
gonets for Debian
----------------
<Possible notes regarding this package - if none, delete this file.>
-- GOSSYS LLC <devteam@gossys.dev> Thu, 08 May 2025 15:39:41 +0500

10
debian/README.source vendored
View file

@ -1,10 +0,0 @@
gonets for Debian
----------------
<This file describes information about the source package, see Debian policy
manual section 4.14. You WILL either need to modify or delete this file.>
-- GOSSYS LLC <devteam@gossys.dev> Thu, 08 May 2025 15:39:41 +0500

4
debian/changelog vendored
View file

@ -1,5 +1,5 @@
gonets (0.2-5) UNRELEASED; urgency=medium gonets (0.3-1) UNRELEASED; urgency=medium
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP> * Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
-- GOSSYS LLC <devteam@gossys.dev> Thu, 08 May 2025 15:39:41 +0500 -- GOSSYS LLC <devteam@gossys.dev> Thu, 19 Jun 2025 13:24:42 +0500

9
debian/control vendored
View file

@ -6,7 +6,7 @@ Rules-Requires-Root: no
Build-Depends: Build-Depends:
debhelper-compat (= 13), debhelper-compat (= 13),
Standards-Version: 4.6.2 Standards-Version: 4.6.2
Homepage: <insert the upstream URL, if relevant> Homepage: dev.gossys.dev
#Vcs-Browser: https://salsa.debian.org/debian/gonets #Vcs-Browser: https://salsa.debian.org/debian/gonets
#Vcs-Git: https://salsa.debian.org/debian/gonets.git #Vcs-Git: https://salsa.debian.org/debian/gonets.git
@ -14,5 +14,8 @@ Package: gonets
Architecture: any Architecture: any
Depends: Depends:
${shlibs:Depends}, ${shlibs:Depends},
${misc:Depends} ${misc:Depends},
Description: Панель администрирования Linux. pip,
nbtscan,
python3.11-dev
Description: Приложение администрирования Linux.

6
debian/files vendored
View file

@ -1,3 +1,3 @@
gonets-dbgsym_0.2-5_amd64.deb debug optional automatic=yes gonets-dbgsym_0.3-1_amd64.deb debug optional automatic=yes
gonets_0.2-5_amd64.buildinfo unknown optional gonets_0.3-1_amd64.buildinfo unknown optional
gonets_0.2-5_amd64.deb unknown optional gonets_0.3-1_amd64.deb unknown optional

View file

@ -1,2 +1,2 @@
README.source
README.Debian README.Debian
README.source

View file

@ -1,10 +1,10 @@
Package: gonets Package: gonets
Version: 0.2-5 Version: 0.3-1
Architecture: amd64 Architecture: amd64
Maintainer: GOSSYS LLC <devteam@gossys.dev> Maintainer: GOSSYS LLC <devteam@gossys.dev>
Installed-Size: 267506 Installed-Size: 343612
Depends: libasound2 (>= 1.0.17), libatk-bridge2.0-0 (>= 2.5.3), libatk1.0-0 (>= 2.32.0), libatspi2.0-0 (>= 2.9.90), libc6 (>= 2.34), libcairo2 (>= 1.6.0), libcups2 (>= 1.7.0), libdbus-1-3 (>= 1.9.14), libexpat1 (>= 2.1~beta3), libgbm1 (>= 17.1.0~rc2), libgcc-s1 (>= 3.0), libglib2.0-0 (>= 2.37.3), libgtk-3-0 (>= 3.9.10), libnspr4 (>= 2:4.9-2~), libnss3 (>= 2:3.30), libpango-1.0-0 (>= 1.14.0), libudev1 (>= 183), libx11-6 (>= 2:1.4.99.1), libxcb1 (>= 1.9.2), libxcomposite1 (>= 1:0.4.5), libxdamage1 (>= 1:1.1), libxext6, libxfixes3, libxkbcommon0 (>= 0.5.0), libxrandr2 Depends: libasound2 (>= 1.0.17), libatk-bridge2.0-0 (>= 2.5.3), libatk1.0-0 (>= 2.32.0), libatspi2.0-0 (>= 2.9.90), libc6 (>= 2.34), libcairo2 (>= 1.6.0), libcups2 (>= 1.7.0), libdbus-1-3 (>= 1.9.14), libexpat1 (>= 2.1~beta3), libgbm1 (>= 17.1.0~rc2), libgcc-s1 (>= 3.0), libglib2.0-0 (>= 2.37.3), libgtk-3-0 (>= 3.9.10), libnspr4 (>= 2:4.9-2~), libnss3 (>= 2:3.30), libpango-1.0-0 (>= 1.14.0), libudev1 (>= 183), libx11-6 (>= 2:1.4.99.1), libxcb1 (>= 1.9.2), libxcomposite1 (>= 1:0.4.5), libxdamage1 (>= 1:1.1), libxext6, libxfixes3, libxkbcommon0 (>= 0.5.0), libxrandr2, pip, nbtscan, python3.11-dev
Section: unknown Section: unknown
Priority: optional Priority: optional
Homepage: <insert the upstream URL, if relevant> Homepage: dev.gossys.dev
Description: Панель администрирования Linux. Description: Приложение администрирования Linux.

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -e set -e
mkdir -p /etc/gonets/certs
pip3 install \ pip3 install \
pydbus==0.6.0 \ pydbus==0.6.0 \
@ -20,13 +21,14 @@ pip3 install \
passlib[bcrypt]==1.7.4 \ passlib[bcrypt]==1.7.4 \
python-multipart==0.0.20 \ python-multipart==0.0.20 \
bcrypt==4.3.0 \ bcrypt==4.3.0 \
mac-vendor-lookup==0.1.12 \
pywifi==1.1.12 \
manuf==1.1.5 \
--break-system-packages || { --break-system-packages || {
echo "Ошибка: Не удалось установить Python-зависимости" >&2 echo "Ошибка: Не удалось установить Python-зависимости" >&2
exit 1 exit 1
} }
mkdir -p /etc/gonets/certs
openssl genrsa -out /etc/gonets/certs/gonets.key 2048 openssl genrsa -out /etc/gonets/certs/gonets.key 2048
openssl req -new -key /etc/gonets/certs/gonets.key -out /etc/gonets/certs/gonets.csr -subj "/" openssl req -new -key /etc/gonets/certs/gonets.key -out /etc/gonets/certs/gonets.csr -subj "/"
openssl x509 -req -days 365 -in /etc/gonets/certs/gonets.csr -signkey /etc/gonets/certs/gonets.key -out /etc/gonets/certs/gonets.crt openssl x509 -req -days 365 -in /etc/gonets/certs/gonets.csr -signkey /etc/gonets/certs/gonets.key -out /etc/gonets/certs/gonets.crt

View file

@ -1 +1 @@
libvulkan 1 gonets (>= 0.2) libvulkan 1 gonets (>= 0.3)

View file

@ -1,6 +0,0 @@
gonets for Debian
----------------
<Possible notes regarding this package - if none, delete this file.>
-- GOSSYS LLC <devteam@gossys.dev> Thu, 08 May 2025 15:39:41 +0500

Some files were not shown because too many files have changed in this diff Show more