Версия 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 { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import { NetworkService } from './src/api/request.ts';
import { useIpStore } from './src/store';
import { useEffect } from 'react';
import { useAdminStore } from './src/store/adminStore.ts';
import {useCheckAdmin} from './src/utils/checkAdmin.ts'
import {

View file

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

View file

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

View file

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

View file

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

View file

@ -2,19 +2,21 @@ import React, { useEffect, useRef } from 'react';
interface SignalSVGProps {
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);
useEffect(() => {
// Устанавливаем цвета в зависимости от signal
const colors = {
'--st1-color': signal >= 1 ? 'white' : 'black',
'--st2-color': signal >= 2 ? 'white' : 'black',
'--st3-color': signal >= 3 ? 'white' : 'black',
'--st4-color': signal >= 4 ? 'white' : 'black',
'--st5-color': signal === 5 ? 'white' : 'black',
'--st1-color': signal >= 1 ? color : 'black',
'--st2-color': signal >= 2 ? color : 'black',
'--st3-color': signal >= 3 ? color : 'black',
'--st4-color': signal >= 4 ? color : 'black',
'--st5-color': signal === 5 ? color : 'black',
};
// Применяем стили к SVG
@ -35,8 +37,8 @@ const SignalSVG: React.FC<SignalSVGProps> = ({ signal }) => {
y="0px"
viewBox="0 0 296 235"
xmlSpace="preserve"
width="20px" // Фиксированная ширина
height="20px"
width={size} // Фиксированная ширина
height={size}
>
<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}
id="newIp"
value={ipData.newIp}
onChange={(id, value) => handleChange(id, value)}
onChange={(value) => handleChange('newIp', value)}
onBlur={handleBlur}
onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}}
@ -203,7 +203,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
placeholderText={data.ipData.ipGate}
id="newGate"
value={ipData.newGate}
onChange={(id, value) => handleChange(id, value)}
onChange={(value) => handleChange('newGate', value)}
onBlur={handleBlur}
onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}}
@ -221,7 +221,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
placeholderText={data.ipData.ipDns.join(', ')}
id="newDns"
value={ipData.newDns.join(', ')}
onChange={(id, value) => handleChange(id, value)}
onChange={( value) => handleChange('newDns', value)}
onBlur={handleBlur}
onFocus={handleFocus}
style={{fontSize: '12px', padding: '4px 10px'}}

View file

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

View file

@ -18,7 +18,7 @@ export const Network = styled.div<Props>`
flex-direction: column;
gap: 10px;
visibility: visible;
overflow: hidden;
p{
margin: 0;
}
@ -29,8 +29,6 @@ export const TitleContainer = styled.div<Props>`
justify-content: space-between;
align-items: center;
background: ${(props) => props.$backgroundcolor};
border-top-left-radius: 8px;
border-top-right-radius: 8px;
color: white;
padding: 15px;
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='Введите адрес узла...'
id="newIp"
value={ipAddress}
onChange={(id, value) => setIpAddress(value)}
onChange={(value) => setIpAddress(value)}
/>
{error && (
<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 {
const result = await NetworkService.saveIp(ipAddress, data);
if (result.status === 200) {
if(ipAddress!=='localhost'){
setIpAddress(ipData.newIp)
const updatedDevices = devices.map(device =>
@ -118,6 +119,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
: device
);
useDevicesStore.setState({ devices: updatedDevices });
}
onClose()
}

View file

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

View file

@ -3,6 +3,10 @@ import { IpConfirm, ConnectionError, AddDevice, RightsConfirm } from "./modalCon
import { ModalMap } from "./windowsType";
import Loader from "../UI/loader";
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 = {
isOpen: boolean,
@ -35,7 +39,34 @@ export default function WindowComponent({ isOpen, type, data }: Props) {
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'] && (
<AddDevice onSave={data?.onSave} onClose={closeLoader} />
)}

View file

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

View file

@ -5,5 +5,9 @@ export const ModalMap: ModalType = {
'2': 'loader',
'3': 'connectionError',
'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() }
} catch (err) {
console.error('Error:', err);
} finally {
}
}
@ -146,7 +145,7 @@ export const CreateUser = () => {
placeholderText='Введите пароль...'
id="password"
value={user.password}
onChange={(id, value) => handleChange(id, value)}
onChange={(value) => handleChange('password', value)}
/>
<Input
@ -154,7 +153,7 @@ export const CreateUser = () => {
placeholderText='Повторите пароль...'
id="repeatPassword"
value={repeatPassword}
onChange={(id, value) => handleChange(id, value)}
onChange={(value) => handleChange('repeatPassword', value)}
style={{ marginTop: '10px' }}
/>
{!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) => (
<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>
<Button>
<IconContainer>
<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;
gap: 15px;
padding: 20px;
width: 210px;
p{
margin: 0;
}
@ -196,12 +196,12 @@ export const DevicesBar = () => {
<DeviceBlock>
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
<p>Текущее устройство</p>
<p>Корневой адрес</p>
</DeviceControl>
{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>

View file

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

View file

@ -9,6 +9,8 @@ import { workSpaceNames } from "../../const/workspace/names";
import { Header } from "./modules/appModules/header";
import { ReactFlowProvider } from "@xyflow/react";
import About from "./modules/appModules/about";
import UserScanner from "./modules/appModules/usersScanner/userScanner";
import {WifiScanner} from "./modules/appModules/wifiScanner/WifiScanner";
const Section = styled.div`
display: flex;
@ -75,6 +77,12 @@ export default function WorkSpace() {
from={"helloWindow"}
/>
)}
{workModule === workSpaceNames[5] && (
<UserScanner />
)}
{workModule === workSpaceNames[6] && (
<WifiScanner />
)}
{workModule === workSpaceNames[3] &&
<WorkSection>
<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 checkCircle from '@iconify/icons-ic/round-check-circle';
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 noAdmin from '@iconify/icons-ic/baseline-remove-moderator'
import refresh from '@iconify/icons-ic/baseline-refresh';
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 = {
[buttonNames[1]]: exitToApp,
@ -30,8 +33,10 @@ export const ICONS = {
[buttonNames[10]]: admin,
[buttonNames[11]]: refresh,
[buttonNames[12]]: close,
[buttonNames[13]]: noAdmin
[buttonNames[13]]: noAdmin,
[buttonNames[14]]: userSearch,
[buttonNames[15]]: wifiFind,
[buttonNames[16]]: noteEdit,
} as const;
type IconName = keyof typeof ICONS;

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ interface IpAddressState {
export const useIpStore = create<IpAddressState>()(
persist(
(set)=>({
ipAddress: 'localhost',
ipAddress: '192:168.1.147',
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 { useAdminStore } from "../store/adminStore";
import { useEffect } from "react";
import { NetworkService } from "../api/request";
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 начиная с версии 0.0.2
Общий репозиторий для GoNets начиная с версии 0.0.1

1
SRV/.gitignore vendored
View file

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

View file

@ -1,5 +1,5 @@
from fastapi import APIRouter
from api.v1 import ufw, websocket, ip, network, auth
from .v1 import ufw, websocket, ip, network, auth, subnet
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(network.router, prefix="/network", tags=["Network"])
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
except Exception as 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)
@ -36,13 +37,16 @@ async def login(user: UserLogin, request: Request):
client_id = request.client.host
logger.debug(f"Попытка авторизации для client_id {client_id}")
result = auth_service.login(user, client_id)
logger.info(f"Успешная авторизация для client_id {client_id}, session_id: {result['session_id']}")
logger.info(
f"Успешная авторизация для client_id {client_id}, session_id: {result['session_id']}"
)
return result
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось авторизовать пользователя")
raise HTTPException(status_code=500,
detail="Не удалось авторизовать пользователя")
@router.get("/me")
@ -55,13 +59,15 @@ async def get_current_user(request: Request):
return {
"user": result["user"],
"access_token": result["access_token"],
"token_type": "bearer"
"token_type": "bearer",
}
except HTTPException as e:
raise e
except Exception as e:
logger.error(f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Не удалось получить данные пользователя")
logger.error(
f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500,
detail="Не удалось получить данные пользователя")
@router.get("/logout")
@ -75,7 +81,8 @@ async def logout(request: Request):
raise e
except Exception as 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")
@ -90,25 +97,34 @@ async def check_users_file():
return {
"exists": False,
"has_users": False,
"message": "Файл users.json не существует"
"message": "Файл users.json не существует",
}
data = storage._read_data()
has_users = len(data.get("users", [])) > 0
if has_users:
logger.info(f"Файл {file_path} существует и содержит пользователей")
logger.info(
f"Файл {file_path} существует и содержит пользователей")
return {
"exists": True,
"has_users": True,
"message": "Файл users.json существует и содержит пользователей"
"message":
"Файл users.json существует и содержит пользователей",
}
else:
logger.info(f"Файл {file_path} существует, но не содержит пользователей")
logger.info(
f"Файл {file_path} существует, но не содержит пользователей")
return {
"exists": True,
"has_users": False,
"message": "Файл users.json существует, но не содержит пользователей"
"exists":
True,
"has_users":
False,
"message":
"Файл users.json существует, но не содержит пользователей",
}
except Exception as e:
logger.error(f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}")
raise HTTPException(status_code=500, detail="Ошибка при проверке файла")
logger.error(
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(
"/zones",
response_model=List[str],
responses={500: {"model": ErrorResponse}},
responses={500: {
"model": ErrorResponse
}},
summary="Get all available zones",
)
async def get_zones() -> List[str]:
@ -53,7 +55,9 @@ async def get_zones() -> List[str]:
@router.get(
"/zones/default",
response_model=str,
responses={500: {"model": ErrorResponse}},
responses={500: {
"model": ErrorResponse
}},
summary="Get default zone",
)
async def get_default_zone() -> str:
@ -75,7 +79,14 @@ async def get_default_zone() -> str:
@router.put(
"/zones/default/{zone}",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Set default zone",
)
async def set_default_zone(
@ -94,9 +105,8 @@ async def set_default_zone(
firewalld.set_default_zone(zone)
return SuccessResponse(message=f"Default zone set to {zone}")
except ValueError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' not found")
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -107,10 +117,18 @@ async def set_default_zone(
@router.get(
"/zones/{zone}",
response_model=FirewallZone,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
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.
@ -122,16 +140,13 @@ async def get_zone(zone: str = Path(..., description="Zone name")) -> FirewallZo
"""
try:
zone_info = firewalld.get_zone_settings(zone)
return FirewallZone(
name=zone,
return FirewallZone(name=zone,
target=FirewallTarget.DEFAULT,
interfaces=[],
**zone_info
)
**zone_info)
except ValueError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' not found")
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -142,10 +157,13 @@ async def get_zone(zone: str = Path(..., description="Zone name")) -> FirewallZo
@router.get(
"/zones/{zone}/services",
response_model=List[str],
responses={500: {"model": ErrorResponse}},
responses={500: {
"model": ErrorResponse
}},
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.
@ -167,7 +185,14 @@ async def get_services(zone: str = Path(..., description="Zone name")) -> List[s
@router.post(
"/zones/{zone}/services",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Add service to zone",
)
async def add_service(
@ -186,11 +211,11 @@ async def add_service(
"""
try:
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:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' not found")
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -201,7 +226,14 @@ async def add_service(
@router.delete(
"/zones/{zone}/services/{service}",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Remove service from zone",
)
async def remove_service(
@ -220,7 +252,8 @@ async def remove_service(
"""
try:
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:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -236,10 +269,13 @@ async def remove_service(
@router.get(
"/zones/{zone}/ports",
response_model=List[str],
responses={500: {"model": ErrorResponse}},
responses={500: {
"model": ErrorResponse
}},
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.
@ -262,9 +298,15 @@ async def get_ports(zone: str = Path(..., description="Zone name")) -> List[str]
"/zones/{zone}/ports",
response_model=SuccessResponse,
responses={
400: {"model": ErrorResponse},
404: {"model": ErrorResponse},
500: {"model": ErrorResponse},
400: {
"model": ErrorResponse
},
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
},
},
summary="Add port to zone",
)
@ -285,10 +327,11 @@ async def add_port(
try:
firewalld.add_port(zone, rule.port, rule.protocol.value, rule.timeout)
return SuccessResponse(
message=f"Port {rule.port}/{rule.protocol.value} added to zone {zone}"
)
message=
f"Port {rule.port}/{rule.protocol.value} added to zone {zone}")
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e))
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -299,7 +342,14 @@ async def add_port(
@router.delete(
"/zones/{zone}/ports",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Remove port from zone",
)
async def remove_port(
@ -319,12 +369,11 @@ async def remove_port(
try:
firewalld.remove_port(zone, rule.port, rule.protocol.value)
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:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' not found")
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -335,10 +384,13 @@ async def remove_port(
@router.get(
"/zones/{zone}/sources",
response_model=List[str],
responses={500: {"model": ErrorResponse}},
responses={500: {
"model": ErrorResponse
}},
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.
@ -360,7 +412,14 @@ async def get_sources(zone: str = Path(..., description="Zone name")) -> List[st
@router.post(
"/zones/{zone}/sources",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Add source to zone",
)
async def add_source(
@ -379,11 +438,11 @@ async def add_source(
"""
try:
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:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail=f"Zone '{zone}' not found")
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
@ -394,7 +453,14 @@ async def add_source(
@router.delete(
"/zones/{zone}/sources/{source}",
response_model=SuccessResponse,
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
responses={
404: {
"model": ErrorResponse
},
500: {
"model": ErrorResponse
}
},
summary="Remove source from zone",
)
async def remove_source(
@ -413,7 +479,8 @@ async def remove_source(
"""
try:
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:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -429,7 +496,9 @@ async def remove_source(
@router.post(
"/reload",
response_model=SuccessResponse,
responses={500: {"model": ErrorResponse}},
responses={500: {
"model": ErrorResponse
}},
summary="Reload firewall rules",
)
async def reload_firewall() -> SuccessResponse:

View file

@ -5,6 +5,7 @@ from services.ip_service import IPService
router = APIRouter()
ip_service = IPService()
@router.post("/check_ip")
async def check_ip_config(data: Dict[str, Any] = Body(...)):
"""Проверяет доступность IP-адреса"""
@ -12,26 +13,44 @@ async def check_ip_config(data: Dict[str, Any] = Body(...)):
result = await ip_service.check_ip_config(data)
return result
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")
async def apply_ip_config(data: Dict[str, Any] = Body(...)):
"""Применяет проверенные IP-настройки"""
try:
await ip_service.apply_ip_config(data)
return {"success": True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены"}
return {
"success":
True,
"message":
f"Настройки сетевого профиля для {data['connection_id']} применены",
}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"Ошибка проверки IP конфигурации: {str(e)}")
@router.post("/save_ip")
async def save_ip_config(data: Dict[str, Any] = Body(...)):
"""Применяет проверенные IP-настройки (Нужен для того, чтобы откатить настройки сетевого профиля обратно)"""
try:
await ip_service.apply_ip_config(data)
return {"success": True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены"}
return {
"success":
True,
"message":
f"Настройки сетевого профиля для {data['connection_id']} применены",
}
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
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 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)
logger = logging.getLogger(__name__)
@ -19,19 +25,29 @@ class ContainerPositionManager:
def __init__(self):
self.positions: Dict[str, Dict[str, Union[int, bool]]] = {
'activation': {'x': 0, 'y': 0, 'isOpen': False},
'deactivation': {'x': 0, 'y': 0, 'isOpen': False}
"activation": {
"x": 0,
"y": 0,
"isOpen": False
},
"deactivation": {
"x": 0,
"y": 0,
"isOpen": False
},
}
def get_position_and_state(self, container_name: str) -> Tuple[int, int, bool]:
def get_position_and_state(self,
container_name: str) -> Tuple[int, int, bool]:
container = self.positions.get(container_name)
if not container:
raise ValueError(f"Неверное имя контейнера: {container_name}")
return container['x'], container['y'], container['isOpen']
return container["x"], container["y"], container["isOpen"]
def update_position(self, container_id: int, x: int, y: int, isOpen: bool) -> None:
container_name = 'activation' if container_id == 1 else 'deactivation'
self.positions[container_name] = {'x': x, 'y': y, 'isOpen': isOpen}
def update_position(self, container_id: int, x: int, y: int,
isOpen: bool) -> None:
container_name = "activation" if container_id == 1 else "deactivation"
self.positions[container_name] = {"x": x, "y": y, "isOpen": isOpen}
container_manager = ContainerPositionManager()
@ -45,44 +61,50 @@ async def get_network_data() -> Dict[str, Dict]:
try:
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')
deactivation_x, deactivation_y, deactivation_is_open = container_manager.get_position_and_state('deactivation')
activation_x, activation_y, activation_is_open = (
container_manager.get_position_and_state("activation"))
deactivation_x, deactivation_y, deactivation_is_open = (
container_manager.get_position_and_state("deactivation"))
activation = Activation(
id=1,
interfaces=activation_interfaces,
container_x=activation_x,
container_y=activation_y,
isOpen=activation_is_open
isOpen=activation_is_open,
)
deactivation = Deactivation(
id=2,
interfaces=deactivation_interfaces,
container_x=deactivation_x,
container_y=deactivation_y,
isOpen=deactivation_is_open
isOpen=deactivation_is_open,
)
return {
"activation": activation.model_dump(),
"deactivation": deactivation.model_dump()
"deactivation": deactivation.model_dump(),
}
except Exception as 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[
List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]:
def _split_interfaces(
interfaces: List[Union[NetworkInterface, WiFiDevice]],
) -> Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[
NetworkInterface, WiFiDevice]]]:
"""
Разделяет интерфейсы на активные и неактивные
"""
activation_interfaces = []
deactivation_interfaces = []
for interface in interfaces:
if interface.device_state == '100':
if interface.device_state == "100":
activation_interfaces.append(interface)
else:
deactivation_interfaces.append(interface)
@ -90,15 +112,20 @@ def _split_interfaces(interfaces: List[Union[NetworkInterface, WiFiDevice]]) ->
@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:
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):
raise ValueError("Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen")
raise ValueError(
"Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen"
)
container_id = data["container_id"]
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"]
# Проверка типов
if not isinstance(container_id, int) or not isinstance(container_x, int) or not isinstance(container_y,
int) or not isinstance(
is_open, bool):
if (not isinstance(container_id, int)
or not isinstance(container_x, int)
or not isinstance(container_y, int)
or not isinstance(is_open, bool)):
raise ValueError("Неверный тип данных в запросе")
container_manager.update_position(container_id, container_x, container_y, is_open)
return {"success": True, "message": "Позиция и состояние контейнера обновлены"}
container_manager.update_position(container_id, container_x,
container_y, is_open)
return {
"success": True,
"message": "Позиция и состояние контейнера обновлены"
}
except Exception as e:
logger.error(f"Ошибка обновления позиции контейнера: {e}")
raise HTTPException(status_code=400, detail=f"Не удалось обновить положение контейнера: {str(e)}")
raise HTTPException(
status_code=400,
detail=f"Не удалось обновить положение контейнера: {str(e)}",
)
@router.get("/get_hostname")
@ -125,4 +160,5 @@ async def get_hostname():
return hostname_info
except Exception as 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 services.ufw_service import UFWService
from models.ufw import UFWRulesResponse, UFWStatus, UFWRule
from typing import List
from pydantic import BaseModel, Field
from models.ufw import UFWRulesResponse, UFWStatus, UFWRule
from services.ufw_service import UFWService
router = APIRouter()
class RuleNumbers(BaseModel):
numbers: List[int] = Field(..., description="Список номеров правил для удаления")
numbers: List[int] = Field(
..., description="Список номеров правил для удаления")
@router.get("/rules", response_model=UFWRulesResponse)
async def get_ufw_rules():
@ -20,6 +24,7 @@ async def get_ufw_rules():
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.get("/status", response_model=UFWStatus)
async def get_ufw_status():
"""
@ -31,6 +36,7 @@ async def get_ufw_status():
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/enable")
async def enable_ufw():
"""
@ -42,10 +48,12 @@ async def enable_ufw():
if success:
return {"status": "success", "message": "UFW успешно включен"}
else:
raise HTTPException(status_code=500, detail="Не удалось включить UFW")
raise HTTPException(status_code=500,
detail="Не удалось включить UFW")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/disable")
async def disable_ufw():
"""
@ -57,10 +65,12 @@ async def disable_ufw():
if success:
return {"status": "success", "message": "UFW успешно выключен"}
else:
raise HTTPException(status_code=500, detail="Не удалось выключить UFW")
raise HTTPException(status_code=500,
detail="Не удалось выключить UFW")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/rules/in")
async def add_ufw_in_rules(rules: List[UFWRule]):
"""
@ -73,23 +83,29 @@ async def add_ufw_in_rules(rules: List[UFWRule]):
if not status.enabled:
raise HTTPException(
status_code=400,
detail="UFW отключен. Включите UFW перед добавлением правил."
detail="UFW отключен. Включите UFW перед добавлением правил.",
)
result = ufw_service.add_rules(rules)
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"])
return {
"status": "success",
"message": f"Добавлено {successful_rules} из {total_rules} правил",
"details": result
"message":
f"Добавлено {successful_rules} из {total_rules} правил",
"details": result,
}
else:
raise HTTPException(status_code=500, detail=result.get("error", "Не удалось добавить правила"))
raise HTTPException(
status_code=500,
detail=result.get("error", "Не удалось добавить правила"),
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.post("/rules/out")
async def add_ufw_out_rules(rules: List[UFWRule]):
"""
@ -102,17 +118,23 @@ async def add_ufw_out_rules(rules: List[UFWRule]):
if not status.enabled:
raise HTTPException(
status_code=400,
detail="UFW отключен. Включите UFW перед добавлением правил."
detail="UFW отключен. Включите UFW перед добавлением правил.",
)
success = ufw_service.add_ufw_out_rules(rules)
if success:
return {"status": "success", "message": "Исходящие правила UFW успешно добавлены"}
return {
"status": "success",
"message": "Исходящие правила UFW успешно добавлены",
}
else:
raise HTTPException(status_code=500, detail="Не удалось добавить исходящие правила UFW")
raise HTTPException(
status_code=500,
detail="Не удалось добавить исходящие правила UFW")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
@router.delete("/rules")
async def delete_ufw_rules(rule_numbers: RuleNumbers):
"""
@ -124,8 +146,9 @@ async def delete_ufw_rules(rule_numbers: RuleNumbers):
if result["success"]:
return {
"status": "success",
"message": f"Удалено {result['successful']} из {result['total']} правил",
"details": result
"message":
f"Удалено {result['successful']} из {result['total']} правил",
"details": result,
}
else:
raise HTTPException(status_code=500, detail=result["error"])

View file

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

View file

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

View file

@ -7,29 +7,33 @@ import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def init_config():
"""Создаёт конфигурационный файл с SECRET_KEY"""
config_path = "/etc/gonets/config.ini"
config_dir = os.path.dirname(config_path)
if os.path.exists(config_path):
logger.info(f"Конфигурационный файл {config_path} уже существует, пропускаем инициализацию")
logger.info(
f"Конфигурационный файл {config_path} уже существует, пропускаем инициализацию"
)
return
try:
secret_key = secrets.token_urlsafe(32)
os.makedirs(config_dir, exist_ok=True)
config = configparser.ConfigParser()
config["JWT"] = {
"secret_key": secret_key
}
config["JWT"] = {"secret_key": secret_key}
with open(config_path, "w") as config_file:
config.write(config_file)
os.chmod(config_path, 0o600)
logger.info(f"Создан конфигурационный файл {config_path}")
except Exception as e:
logger.error(f"Ошибка при создании конфигурационного файла: {type(e).__name__} - {str(e)}")
logger.error(
f"Ошибка при создании конфигурационного файла: {type(e).__name__} - {str(e)}"
)
raise
if __name__ == "__main__":
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
from typing import Tuple
class CLIExecutor:
"""
Класс для выполнения CLI-команд
"""
@staticmethod
def run(command: list) -> Tuple[int, str]:
"""
@ -20,6 +22,6 @@ class CLIExecutor:
command,
stdout=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__":
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 typing import Optional
class UserCreate(BaseModel):
"""
Модель созданного пользователя
Атрибуты:
username (str): Имя пользователя
password (str): Пароль пользователя
is_admin (bool): Административный доступ
"""
username: str
password: str
is_admin: bool = False
class UserLogin(BaseModel):
"""
Модель авторизации пользователя
Атрибуты:
username (str): Имя пользователя
password (str): Пароль пользователя
"""
username: str
password: str
class UserResponse(BaseModel):
"""
Модель проверки наличия пользователя
Атрибуты:
id (int): Идентификатор
username (str): Имя пользователя
is_admin (bool): Административный доступ
"""
id: int
username: str
is_admin: bool
class Token(BaseModel):
"""
Токен пользователя
Атрибуты:
access_token (str): Токен пользователя (захэширован)
token_type (str): Тип токена
session_id (Optional[str]): Идентификатор сессии
user (Optional[UserResponse]): Пользователь на основе проверки наличия пользователя
"""
access_token: str
token_type: str
session_id: Optional[str] = None

View file

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

View file

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

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.
"""
TCP = "tcp"
UDP = "udp"
BOTH = "both"
@ -27,6 +28,7 @@ class Action(str, Enum):
DENY: Запретить трафик без уведомления
REJECT: Запретить трафик с уведомлением отправителю
"""
ALLOW = "allow"
DENY = "deny"
REJECT = "reject"
@ -40,12 +42,14 @@ class Direction(str, Enum):
IN: Входящий трафик
OUT: Исходящий трафик
"""
IN = "in"
OUT = "out"
class UFWRule(BaseModel):
"""Модель правила UFW"""
action: Action
direction: Direction
from_ip: str
@ -70,18 +74,23 @@ class UFWRule(BaseModel):
"""
if isinstance(v, int):
if not (1 <= v <= 65535):
raise ValueError(f"Порт должен быть в диапазоне 1-65535, получено: {v}")
raise ValueError(
f"Порт должен быть в диапазоне 1-65535, получено: {v}")
return v
elif isinstance(v, str):
try:
start, end = map(int, v.split(':'))
start, end = map(int, v.split(":"))
if not (1 <= start <= end <= 65535):
raise ValueError
return v
except (ValueError, TypeError):
raise ValueError(f"Некорректный диапазон портов: {v}. Формат: start:end, где 1 <= start <= end <= 65535")
raise ValueError(
f"Некорректный диапазон портов: {v}. Формат: start:end, где 1 <= start <= end <= 65535"
)
else:
raise ValueError(f"Порт должен быть числом или строкой с диапазоном, получено: {type(v)}")
raise ValueError(
f"Порт должен быть числом или строкой с диапазоном, получено: {type(v)}"
)
@validator("from_ip", "to_ip")
def validate_ip(cls, v):
@ -103,8 +112,11 @@ class UFWRule(BaseModel):
from_ip = ipaddress.ip_network(values["from_ip"], strict=False)
to_ip = ipaddress.ip_network(v, strict=False)
if isinstance(from_ip, ipaddress.IPv6Network) != isinstance(to_ip, ipaddress.IPv6Network):
raise ValueError("Несовместимые версии IP: нельзя смешивать IPv4 и IPv6 в одном правиле")
if isinstance(from_ip, ipaddress.IPv6Network) != isinstance(
to_ip, ipaddress.IPv6Network):
raise ValueError(
"Несовместимые версии IP: нельзя смешивать IPv4 и IPv6 в одном правиле"
)
return v
@ -139,7 +151,8 @@ class UFWRule(BaseModel):
cmd_parts.extend(["to", self.to_ip])
# Добавляем порт и протокол
port_str = str(self.port) # Работает как с числами, так и с диапазонами
port_str = str(
self.port) # Работает как с числами, так и с диапазонами
cmd_parts.extend(["port", port_str])
# Добавляем протокол
@ -183,26 +196,27 @@ class UFWRule(BaseModel):
match = re.match(pattern, line, re.VERBOSE | re.IGNORECASE)
if not match:
raise ValueError(f"Невозможно разобрать строку правила UFW: {line}")
raise ValueError(
f"Невозможно разобрать строку правила UFW: {line}")
groups = match.groupdict()
# Преобразуем протокол в enum
proto_str = groups['protocol'].lower()
protocol = Protocol.BOTH if proto_str == "tcp/udp" else \
Protocol.UDP if proto_str == "udp" else Protocol.TCP
proto_str = groups["protocol"].lower()
protocol = (Protocol.BOTH if proto_str == "tcp/udp" else
Protocol.UDP if proto_str == "udp" else Protocol.TCP)
# Преобразуем действие в enum (игнорируем регистр)
action_str = groups['action'].lower()
action = Action.ALLOW if action_str == "allow" else \
Action.DENY if action_str == "deny" else Action.REJECT
action_str = groups["action"].lower()
action = (Action.ALLOW if action_str == "allow" else
Action.DENY if action_str == "deny" else Action.REJECT)
# Преобразуем направление в enum
direction_str = groups['direction'].lower()
direction_str = groups["direction"].lower()
direction = Direction.IN if direction_str == "in" else Direction.OUT
# Обрабатываем IP адреса
from_ip = groups['from_ip'].strip()
from_ip = groups["from_ip"].strip()
if from_ip.lower() == "anywhere":
from_ip = "0.0.0.0/0"
@ -210,8 +224,8 @@ class UFWRule(BaseModel):
to_ip = "0.0.0.0/0" # По умолчанию anywhere
# Обрабатываем порт
port = groups['port']
if ':' in port:
port = groups["port"]
if ":" in port:
# Оставляем как строку для диапазона
port = port.strip()
else:
@ -219,7 +233,8 @@ class UFWRule(BaseModel):
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(
action=action,
@ -228,7 +243,7 @@ class UFWRule(BaseModel):
to_ip=to_ip,
port=port,
protocol=protocol,
comment=comment
comment=comment,
)
@ -241,8 +256,10 @@ class UFWRulesResponse(BaseModel):
rules: Список правил UFW
total_rules: Общее количество правил
"""
enabled: bool = Field(..., description="Включен ли UFW")
rules: List[UFWRule] = Field(default_factory=list, description="Список правил UFW")
rules: List[UFWRule] = Field(default_factory=list,
description="Список правил UFW")
@property
def total_rules(self) -> int:
@ -259,5 +276,6 @@ class UFWStatus(BaseModel):
message: Сообщение о статусе
last_checked: Время последней проверки статуса
"""
enabled: bool = Field(..., description="Включен ли UFW")
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]]
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\""]
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]]
name = "bcrypt"
version = "4.3.0"
@ -395,6 +562,120 @@ uvicorn = {version = ">=0.15.0", extras = ["standard"]}
[package.extras]
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]]
name = "h11"
version = "0.14.0"
@ -543,6 +824,37 @@ MarkupSafe = ">=2.0"
[package.extras]
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]]
name = "markdown-it-py"
version = "3.0.0"
@ -651,6 +963,120 @@ files = [
{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]]
name = "netaddr"
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)"]
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]]
name = "pyasn1"
version = "0.4.8"
@ -1059,6 +1593,18 @@ files = [
{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]]
name = "pyyaml"
version = "6.0.2"
@ -1569,7 +2115,126 @@ files = [
{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]
lock-version = "2.1"
python-versions = "^3.11"
content-hash = "a9465e2172fc3fe6ee66a3df67343b3e245e723d4244d8e85534df3ed1696369"
content-hash = "6dfae73c0cdec175259066cdcf6fe6bceefc5ddfc84252b6093d1abf3ec82d58"

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

View file

@ -9,7 +9,9 @@ from datetime import datetime, timedelta
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SessionStorage:
def __init__(self, file_path: str = "/etc/gonets/sessions.json"):
self.file_path = file_path
self._ensure_file_exists()
@ -18,7 +20,8 @@ class SessionStorage:
try:
logger.info(f"Проверка существования файла {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)
logger.info(f"Создание файла {self.file_path}")
with open(self.file_path, "w") as f:
@ -28,7 +31,9 @@ class SessionStorage:
else:
logger.info(f"Файл {self.file_path} уже существует")
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
def _read_data(self) -> Dict:
@ -36,7 +41,9 @@ class SessionStorage:
with open(self.file_path, "r") as f:
return json.load(f)
except Exception as e:
logger.error(f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}")
logger.error(
f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}"
)
raise
def _write_data(self, data: Dict):
@ -44,7 +51,9 @@ class SessionStorage:
with open(self.file_path, "w") as f:
json.dump(data, f, indent=2)
except Exception as e:
logger.error(f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}")
logger.error(
f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}"
)
raise
def create_session(self, username: str, token: str, client_id: str) -> str:
@ -60,13 +69,17 @@ class SessionStorage:
"token": token,
"client_id": client_id,
"created_at": created_at,
"expiry": expiry
"expiry": expiry,
}
self._write_data(data)
logger.info(f"Сессия создана для пользователя {username} с session_id {session_id}")
logger.info(
f"Сессия создана для пользователя {username} с session_id {session_id}"
)
return session_id
except Exception as e:
logger.error(f"Ошибка при создании сессии для {username}: {type(e).__name__} - {str(e)}")
logger.error(
f"Ошибка при создании сессии для {username}: {type(e).__name__} - {str(e)}"
)
raise
def get_latest_session(self, client_id: str) -> Optional[Dict]:
@ -95,7 +108,9 @@ class SessionStorage:
self._write_data(data)
return None
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
def delete_session(self, session_id: str):
@ -108,7 +123,9 @@ class SessionStorage:
self._write_data(data)
logger.info(f"Сессия с session_id {session_id} удалена")
except Exception as e:
logger.error(f"Ошибка при удалении сессии {session_id}: {type(e).__name__} - {str(e)}")
logger.error(
f"Ошибка при удалении сессии {session_id}: {type(e).__name__} - {str(e)}"
)
raise
def clean_expired_sessions(self):
@ -127,7 +144,8 @@ class SessionStorage:
# Удаляем истекшие сессии
for session_id in expired_sessions:
del data["sessions"][session_id]
logger.info(f"Удалена истекшая сессия с session_id {session_id}")
logger.info(
f"Удалена истекшая сессия с session_id {session_id}")
# Записываем обновлённые данные
if expired_sessions:
@ -137,5 +155,7 @@ class SessionStorage:
logger.debug("Истекшие сессии не найдены")
except Exception as e:
logger.error(f"Ошибка при очистке истекших сессий: {type(e).__name__} - {str(e)}")
logger.error(
f"Ошибка при очистке истекших сессий: {type(e).__name__} - {str(e)}"
)
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>
-- 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
Source: gonets
Version: 0.2-5
Version: 0.3-1
Auto-Built-Package: debug-symbols
Architecture: amd64
Maintainer: GOSSYS LLC <devteam@gossys.dev>
Installed-Size: 1528
Depends: gonets (= 0.2-5)
Depends: gonets (= 0.3-1)
Section: debug
Priority: optional
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>
-- 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:
debhelper-compat (= 13),
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-Git: https://salsa.debian.org/debian/gonets.git
@ -14,5 +14,8 @@ Package: gonets
Architecture: any
Depends:
${shlibs:Depends},
${misc:Depends}
Description: Панель администрирования Linux.
${misc:Depends},
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_0.2-5_amd64.buildinfo unknown optional
gonets_0.2-5_amd64.deb unknown optional
gonets-dbgsym_0.3-1_amd64.deb debug optional automatic=yes
gonets_0.3-1_amd64.buildinfo unknown optional
gonets_0.3-1_amd64.deb unknown optional

View file

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

View file

@ -1,10 +1,10 @@
Package: gonets
Version: 0.2-5
Version: 0.3-1
Architecture: amd64
Maintainer: GOSSYS LLC <devteam@gossys.dev>
Installed-Size: 267506
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
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, pip, nbtscan, python3.11-dev
Section: unknown
Priority: optional
Homepage: <insert the upstream URL, if relevant>
Description: Панель администрирования Linux.
Homepage: dev.gossys.dev
Description: Приложение администрирования Linux.

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
#!/bin/bash
set -e
mkdir -p /etc/gonets/certs
pip3 install \
pydbus==0.6.0 \
@ -20,13 +21,14 @@ pip3 install \
passlib[bcrypt]==1.7.4 \
python-multipart==0.0.20 \
bcrypt==4.3.0 \
mac-vendor-lookup==0.1.12 \
pywifi==1.1.12 \
manuf==1.1.5 \
--break-system-packages || {
echo "Ошибка: Не удалось установить Python-зависимости" >&2
exit 1
}
mkdir -p /etc/gonets/certs
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 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