Версия 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
10
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
.venv
|
||||||
|
.idea
|
||||||
|
__pycache__
|
||||||
|
.gigaide
|
||||||
|
.vscode
|
||||||
|
.wheels
|
||||||
|
.vendor
|
||||||
|
.python3-deb
|
||||||
|
SRV/storage/devices/devices.json
|
||||||
|
SRV/storage/wifi/wifi.json
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
debian/patches
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
series
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
2
|
|
||||||
|
|
@ -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} не найдено")
|
|
||||||
|
|
@ -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} не найдено")
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
1
|
|
||||||
2
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { HashRouter } from 'react-router-dom';
|
||||||
import { NetworkService } from './src/api/request.ts';
|
|
||||||
import { useIpStore } from './src/store';
|
import { useIpStore } from './src/store';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useAdminStore } from './src/store/adminStore.ts';
|
|
||||||
import {useCheckAdmin} from './src/utils/checkAdmin.ts'
|
import {useCheckAdmin} from './src/utils/checkAdmin.ts'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ function createWindow() {
|
||||||
win.show();
|
win.show();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
win.loadURL('http://localhost:3000');
|
win.loadURL('https://localhost:8001');
|
||||||
win.webContents.openDevTools();
|
win.webContents.openDevTools();
|
||||||
} else {
|
} else {
|
||||||
win.loadFile(path.join(__dirname, 'dist/index.html'));
|
win.loadFile(path.join(__dirname, 'dist/index.html'));
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/icons-ic": "^1.2.13",
|
"@iconify/icons-ic": "^1.2.13",
|
||||||
|
"@iconify/icons-ph": "^1.2.5",
|
||||||
"@iconify/react": "^5.2.0",
|
"@iconify/react": "^5.2.0",
|
||||||
"@reduxjs/toolkit": "^2.6.0",
|
"@reduxjs/toolkit": "^2.6.0",
|
||||||
"@xyflow/react": "^12.3.5",
|
"@xyflow/react": "^12.3.5",
|
||||||
|
|
|
||||||
|
|
@ -82,4 +82,79 @@ export class NetworkService {
|
||||||
return error.response;
|
return error.response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async scanUser(ipAddress: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.get('/subnet/devices');
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async scanWifi(ipAddress: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.get('/subnet/wifi');
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async addNote(ipAddress: string, address:string, note: string,type:string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.post(`/subnet/${type}/${address}/note`, {
|
||||||
|
note: note
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async editNote(ipAddress: string, address:string, note: string,type:string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.put(`/subnet/${type}/${address}/note`, {
|
||||||
|
note: note
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async deleteNote(ipAddress: string, address:string, type:string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.delete(`/subnet/${type}/${address}/note`);
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async statusNetwork(ipAddress: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.get(`/subnet/wifi/status`);
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async connectNetwork(ipAddress: string, password:string, address: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.post(`/subnet/wifi/connect`, {
|
||||||
|
ssid: address,
|
||||||
|
password: password,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static async disconnectNetwork(ipAddress: string, ssid: string): Promise<AxiosResponse> {
|
||||||
|
try {
|
||||||
|
const axiosInstance = this.createAxiosInstance(ipAddress);
|
||||||
|
return await axiosInstance.post(`/subnet/wifi/disconnect`, {
|
||||||
|
ssid: ssid,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.response;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -9,15 +9,15 @@ const Button = styled.button.withConfig({
|
||||||
border: none;
|
border: none;
|
||||||
padding: ${(props) =>
|
padding: ${(props) =>
|
||||||
props.size === "small"
|
props.size === "small"
|
||||||
? "4px 8px"
|
? "10px 28px"
|
||||||
: props.size === "big"
|
: props.size === "big"
|
||||||
? "10px 20px"
|
? "10px 20px"
|
||||||
: "6px 10px"}; // По умолчанию
|
: "12px 20px"}; // По умолчанию
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: ${(props) =>
|
font-size: ${(props) =>
|
||||||
props.size === "small"
|
props.size === "small"
|
||||||
? "12px"
|
? "14px"
|
||||||
: props.size === "big"
|
: props.size === "big"
|
||||||
? "16px"
|
? "16px"
|
||||||
: "14px"}; // По умолчанию
|
: "14px"}; // По умолчанию
|
||||||
|
|
@ -30,7 +30,7 @@ const Button = styled.button.withConfig({
|
||||||
? 'row-reverse'
|
? 'row-reverse'
|
||||||
: 'row'};
|
: 'row'};
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
transition: all .3s ease;
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color: ${(props) => props.$hoverсolor};
|
background-color: ${(props) => props.$hoverсolor};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ type Props = {
|
||||||
placeholderText: string;
|
placeholderText: string;
|
||||||
id: string;
|
id: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (identifier: string, value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onFileChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
type?: string;
|
type?: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
|
@ -33,7 +33,7 @@ const StyledInput = styled.input`
|
||||||
border: 1px solid #adadad;
|
border: 1px solid #adadad;
|
||||||
}
|
}
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 14px;
|
font-size: 18px;
|
||||||
color: ${props=>props.theme.textColor}
|
color: ${props=>props.theme.textColor}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ const Input = React.forwardRef<HTMLInputElement, Props>(({ placeholderText, id,
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(id, value);
|
onChange(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,21 @@ import React, { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
interface SignalSVGProps {
|
interface SignalSVGProps {
|
||||||
signal: number; // Принимаем signal как пропс
|
signal: number; // Принимаем signal как пропс
|
||||||
|
color: string;
|
||||||
|
size: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignalSVG: React.FC<SignalSVGProps> = ({ signal }) => {
|
const SignalSVG: React.FC<SignalSVGProps> = ({ signal, color, size }) => {
|
||||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Устанавливаем цвета в зависимости от signal
|
// Устанавливаем цвета в зависимости от signal
|
||||||
const colors = {
|
const colors = {
|
||||||
'--st1-color': signal >= 1 ? 'white' : 'black',
|
'--st1-color': signal >= 1 ? color : 'black',
|
||||||
'--st2-color': signal >= 2 ? 'white' : 'black',
|
'--st2-color': signal >= 2 ? color : 'black',
|
||||||
'--st3-color': signal >= 3 ? 'white' : 'black',
|
'--st3-color': signal >= 3 ? color : 'black',
|
||||||
'--st4-color': signal >= 4 ? 'white' : 'black',
|
'--st4-color': signal >= 4 ? color : 'black',
|
||||||
'--st5-color': signal === 5 ? 'white' : 'black',
|
'--st5-color': signal === 5 ? color : 'black',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Применяем стили к SVG
|
// Применяем стили к SVG
|
||||||
|
|
@ -35,8 +37,8 @@ const SignalSVG: React.FC<SignalSVGProps> = ({ signal }) => {
|
||||||
y="0px"
|
y="0px"
|
||||||
viewBox="0 0 296 235"
|
viewBox="0 0 296 235"
|
||||||
xmlSpace="preserve"
|
xmlSpace="preserve"
|
||||||
width="20px" // Фиксированная ширина
|
width={size} // Фиксированная ширина
|
||||||
height="20px"
|
height={size}
|
||||||
>
|
>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
{`
|
{`
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -184,7 +184,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
|
||||||
placeholderText={data.ipData.ipAdress}
|
placeholderText={data.ipData.ipAdress}
|
||||||
id="newIp"
|
id="newIp"
|
||||||
value={ipData.newIp}
|
value={ipData.newIp}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={(value) => handleChange('newIp', value)}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
style={{fontSize: '12px', padding: '4px 10px'}}
|
style={{fontSize: '12px', padding: '4px 10px'}}
|
||||||
|
|
@ -203,7 +203,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
|
||||||
placeholderText={data.ipData.ipGate}
|
placeholderText={data.ipData.ipGate}
|
||||||
id="newGate"
|
id="newGate"
|
||||||
value={ipData.newGate}
|
value={ipData.newGate}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={(value) => handleChange('newGate', value)}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
style={{fontSize: '12px', padding: '4px 10px'}}
|
style={{fontSize: '12px', padding: '4px 10px'}}
|
||||||
|
|
@ -221,7 +221,7 @@ export const IpNode = ({ data }: CustomNodeProps) => {
|
||||||
placeholderText={data.ipData.ipDns.join(', ')}
|
placeholderText={data.ipData.ipDns.join(', ')}
|
||||||
id="newDns"
|
id="newDns"
|
||||||
value={ipData.newDns.join(', ')}
|
value={ipData.newDns.join(', ')}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={( value) => handleChange('newDns', value)}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
style={{fontSize: '12px', padding: '4px 10px'}}
|
style={{fontSize: '12px', padding: '4px 10px'}}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ export const NetWorkNode = ({ data }: CustomNodeProps) => {
|
||||||
|
|
||||||
{data.networkData.type == 'WIFI' && data.networkData.state === "Подключено" ? (
|
{data.networkData.type == 'WIFI' && data.networkData.state === "Подключено" ? (
|
||||||
<Status>
|
<Status>
|
||||||
<SignalSVG signal={signal} />
|
<SignalSVG signal={signal} color={'white'} size={'20px'} />
|
||||||
</Status>
|
</Status>
|
||||||
) : (
|
) : (
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export const Network = styled.div<Props>`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
overflow: hidden;
|
||||||
p{
|
p{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -29,8 +29,6 @@ export const TitleContainer = styled.div<Props>`
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: ${(props) => props.$backgroundcolor};
|
background: ${(props) => props.$backgroundcolor};
|
||||||
border-top-left-radius: 8px;
|
|
||||||
border-top-right-radius: 8px;
|
|
||||||
color: white;
|
color: white;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
height: ${(props) => props.$height ? props.$height : 'unset'};
|
height: ${(props) => props.$height ? props.$height : 'unset'};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -39,7 +39,7 @@ export const AddDevice = ({ onSave, onClose }: Props) => {
|
||||||
placeholderText='Введите адрес узла...'
|
placeholderText='Введите адрес узла...'
|
||||||
id="newIp"
|
id="newIp"
|
||||||
value={ipAddress}
|
value={ipAddress}
|
||||||
onChange={(id, value) => setIpAddress(value)}
|
onChange={(value) => setIpAddress(value)}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<p style={{ color: 'red', marginTop: '5px' }}>
|
<p style={{ color: 'red', marginTop: '5px' }}>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -110,6 +110,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
||||||
try {
|
try {
|
||||||
const result = await NetworkService.saveIp(ipAddress, data);
|
const result = await NetworkService.saveIp(ipAddress, data);
|
||||||
if (result.status === 200) {
|
if (result.status === 200) {
|
||||||
|
if(ipAddress!=='localhost'){
|
||||||
setIpAddress(ipData.newIp)
|
setIpAddress(ipData.newIp)
|
||||||
|
|
||||||
const updatedDevices = devices.map(device =>
|
const updatedDevices = devices.map(device =>
|
||||||
|
|
@ -118,6 +119,7 @@ export const IpConfirm = ({ ipData, onClose, smoothClose }: Props) => {
|
||||||
: device
|
: device
|
||||||
);
|
);
|
||||||
useDevicesStore.setState({ devices: updatedDevices });
|
useDevicesStore.setState({ devices: updatedDevices });
|
||||||
|
}
|
||||||
|
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ export const RightsConfirm = ({ onClose }: Props) => {
|
||||||
placeholderText='Введите пароль администратора...'
|
placeholderText='Введите пароль администратора...'
|
||||||
id="password"
|
id="password"
|
||||||
value={user.password}
|
value={user.password}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={(value) => handleChange('password', value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import { IpConfirm, ConnectionError, AddDevice, RightsConfirm } from "./modalCon
|
||||||
import { ModalMap } from "./windowsType";
|
import { ModalMap } from "./windowsType";
|
||||||
import Loader from "../UI/loader";
|
import Loader from "../UI/loader";
|
||||||
import { ModalWindow } from "./style";
|
import { ModalWindow } from "./style";
|
||||||
|
import {NoteDevice} from "./modalContainers/NoteDevice";
|
||||||
|
import { NoteWifi } from "./modalContainers/NoteWifi";
|
||||||
|
import {ConnectNetwork} from "./modalContainers/ConnectNetwork";
|
||||||
|
import {DisconnectConfirm} from "./modalContainers/disconnectConfirm";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
|
|
@ -35,7 +39,34 @@ export default function WindowComponent({ isOpen, type, data }: Props) {
|
||||||
message={data.message}
|
message={data.message}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
{type === ModalMap['6'] && (
|
||||||
|
<NoteDevice
|
||||||
|
type={'devices'}
|
||||||
|
address={data.address}
|
||||||
|
currentNote={data.currentNote}
|
||||||
|
onClose={closeLoader}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{type === ModalMap['7'] && (
|
||||||
|
<NoteWifi
|
||||||
|
type={'wifi'}
|
||||||
|
address={data.address}
|
||||||
|
currentNote={data.currentNote}
|
||||||
|
onClose={closeLoader}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{type === ModalMap['8'] && (
|
||||||
|
<ConnectNetwork
|
||||||
|
address={data.address}
|
||||||
|
onClose={closeLoader}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{type === ModalMap['9'] && (
|
||||||
|
<DisconnectConfirm
|
||||||
|
address={data.address}
|
||||||
|
onClose={closeLoader}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{type === ModalMap['4'] && (
|
{type === ModalMap['4'] && (
|
||||||
<AddDevice onSave={data?.onSave} onClose={closeLoader} />
|
<AddDevice onSave={data?.onSave} onClose={closeLoader} />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export const ModalContainer = styled.div<{height?: string}>`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
height: ${(props) => props.height ? props.height : '200px'};
|
height: ${(props) => props.height ? props.height : '230px'};
|
||||||
width: 520px;
|
width: 520px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -66,5 +66,5 @@ export const ButtonContainer = styled.div`
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 20px;
|
padding: 10px 20px 20px;
|
||||||
`
|
`
|
||||||
|
|
@ -5,5 +5,9 @@ export const ModalMap: ModalType = {
|
||||||
'2': 'loader',
|
'2': 'loader',
|
||||||
'3': 'connectionError',
|
'3': 'connectionError',
|
||||||
'4': 'addDevice',
|
'4': 'addDevice',
|
||||||
'5': 'sudo'
|
'5': 'sudo',
|
||||||
|
'6': 'noteDevice',
|
||||||
|
'7': 'noteWifi',
|
||||||
|
'8': 'connectNetwork',
|
||||||
|
'9': 'disconnectConfirm',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,6 @@ export const CreateUser = () => {
|
||||||
if (result.status === 200) { window.location.reload() }
|
if (result.status === 200) { window.location.reload() }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error:', err);
|
console.error('Error:', err);
|
||||||
} finally {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,7 +145,7 @@ export const CreateUser = () => {
|
||||||
placeholderText='Введите пароль...'
|
placeholderText='Введите пароль...'
|
||||||
id="password"
|
id="password"
|
||||||
value={user.password}
|
value={user.password}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={(value) => handleChange('password', value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -154,7 +153,7 @@ export const CreateUser = () => {
|
||||||
placeholderText='Повторите пароль...'
|
placeholderText='Повторите пароль...'
|
||||||
id="repeatPassword"
|
id="repeatPassword"
|
||||||
value={repeatPassword}
|
value={repeatPassword}
|
||||||
onChange={(id, value) => handleChange(id, value)}
|
onChange={(value) => handleChange('repeatPassword', value)}
|
||||||
style={{ marginTop: '10px' }}
|
style={{ marginTop: '10px' }}
|
||||||
/>
|
/>
|
||||||
{!passwordsMatch && (
|
{!passwordsMatch && (
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import styled from "styled-components"
|
import styled from "styled-components"
|
||||||
import { MainButton } from "../../../UI/buttons/mainButton"
|
import { MainButton } from "../../../UI/buttons/mainButton"
|
||||||
import { useWorkSpaceStore } from "../../../../store"
|
import { useWorkSpaceStore } from "../../../../store"
|
||||||
import { workSpaceNames } from "../../../../const/workspace/names"
|
import { workSpaceNames } from "../../../../const/workspace/names"
|
||||||
import { buttonNames } from "../../../../const/buttons/names"
|
import { buttonNames } from "../../../../const/buttons/names"
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -15,30 +15,30 @@ const Wrapper = styled.div`
|
||||||
background-size: 100%;
|
background-size: 100%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: bottom;
|
background-position: bottom;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 52px;
|
gap: 52px;
|
||||||
margin-bottom: 100px;
|
margin-bottom: 100px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ButtonWrapper = styled.div`
|
const ButtonWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 75px;
|
gap: 75px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ButtonContainer = styled.div`
|
const ButtonContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Title = styled.div`
|
const Title = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
@ -46,9 +46,9 @@ const Title = styled.div`
|
||||||
h1{
|
h1{
|
||||||
font-size: 52px;
|
font-size: 52px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const HelloWindow = () => {
|
export const HelloWindow = () => {
|
||||||
|
|
||||||
const { setWorkModule } = useWorkSpaceStore()
|
const { setWorkModule } = useWorkSpaceStore()
|
||||||
|
|
||||||
|
|
@ -79,4 +79,4 @@ export const HelloWindow = () => {
|
||||||
</Container>
|
</Container>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -50,10 +50,19 @@ export const ToolBar = () => {
|
||||||
{toolBarMenu && toolBarMenu.map((menu) => (
|
{toolBarMenu && toolBarMenu.map((menu) => (
|
||||||
<ToolBarButton menuId={menu.id} key={menu.id} />
|
<ToolBarButton menuId={menu.id} key={menu.id} />
|
||||||
))}
|
))}
|
||||||
|
<Button>
|
||||||
|
<IconContainer>
|
||||||
|
<IconComponent name={buttonNames[14]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[5])} />
|
||||||
|
</IconContainer>
|
||||||
|
</Button>
|
||||||
|
<Button>
|
||||||
|
<IconContainer>
|
||||||
|
<IconComponent name={buttonNames[15]} width="30" height="30" onClick={() => setWorkModule(workSpaceNames[6])} />
|
||||||
|
</IconContainer>
|
||||||
|
</Button>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
|
|
||||||
<Button>
|
<Button>
|
||||||
<IconContainer>
|
<IconContainer>
|
||||||
<IconComponent name={isAdmin ? buttonNames[10] : buttonNames[13]} width="30" height="30" onClick={() => openModal(ModalMap['5'])} />
|
<IconComponent name={isAdmin ? buttonNames[10] : buttonNames[13]} width="30" height="30" onClick={() => openModal(ModalMap['5'])} />
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ const Container = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
width: 210px;
|
||||||
p{
|
p{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -196,12 +196,12 @@ export const DevicesBar = () => {
|
||||||
|
|
||||||
<DeviceBlock>
|
<DeviceBlock>
|
||||||
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
|
<DeviceControl className="unselectable" onClick={() => switchWs(currentDevice.address)}>
|
||||||
<p>Текущее устройство</p>
|
<p>Корневой адрес</p>
|
||||||
</DeviceControl>
|
</DeviceControl>
|
||||||
{currentDevice.hostname == '' ? (
|
{currentDevice.hostname == '' ? (
|
||||||
<Text className="paddingP">Адрес: Загрузка<AnimDot /></Text>
|
<Text className="paddingP">Название: Загрузка<AnimDot /></Text>
|
||||||
) : (
|
) : (
|
||||||
<p className="paddingP">Адрес: {currentDevice.hostname}</p>
|
<p className="paddingP">Название: {currentDevice.hostname}</p>
|
||||||
)}
|
)}
|
||||||
</DeviceBlock>
|
</DeviceBlock>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,15 @@ import { useToolBar } from "../../../../hooks/useToolBar"
|
||||||
import { Space } from "../../../../const/menus/sideBar/components"
|
import { Space } from "../../../../const/menus/sideBar/components"
|
||||||
import styled from "styled-components"
|
import styled from "styled-components"
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div<{isOpen: boolean}>`
|
||||||
background-color: ${(props) => props.theme.secondary};
|
background-color: ${(props) => props.theme.secondary};
|
||||||
width: 250px;
|
width: 250px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
flex: 3;
|
flex: 3;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: ${({isOpen})=>(isOpen ? '250px' : '0')};
|
||||||
|
transition: max-width .4s ease;
|
||||||
`
|
`
|
||||||
|
|
||||||
export const ToolSpace = () => {
|
export const ToolSpace = () => {
|
||||||
|
|
@ -15,12 +19,12 @@ export const ToolSpace = () => {
|
||||||
const name = toolBar?.current || ''
|
const name = toolBar?.current || ''
|
||||||
const { isActive, isOpen } = useToolBar(name)
|
const { isActive, isOpen } = useToolBar(name)
|
||||||
|
|
||||||
if (!isActive || !isOpen) {
|
if (!isActive) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container isOpen={isOpen}>
|
||||||
<Space name={name} />
|
<Space name={name} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import { workSpaceNames } from "../../const/workspace/names";
|
||||||
import { Header } from "./modules/appModules/header";
|
import { Header } from "./modules/appModules/header";
|
||||||
import { ReactFlowProvider } from "@xyflow/react";
|
import { ReactFlowProvider } from "@xyflow/react";
|
||||||
import About from "./modules/appModules/about";
|
import About from "./modules/appModules/about";
|
||||||
|
import UserScanner from "./modules/appModules/usersScanner/userScanner";
|
||||||
|
import {WifiScanner} from "./modules/appModules/wifiScanner/WifiScanner";
|
||||||
|
|
||||||
const Section = styled.div`
|
const Section = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -75,6 +77,12 @@ export default function WorkSpace() {
|
||||||
from={"helloWindow"}
|
from={"helloWindow"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{workModule === workSpaceNames[5] && (
|
||||||
|
<UserScanner />
|
||||||
|
)}
|
||||||
|
{workModule === workSpaceNames[6] && (
|
||||||
|
<WifiScanner />
|
||||||
|
)}
|
||||||
{workModule === workSpaceNames[3] &&
|
{workModule === workSpaceNames[3] &&
|
||||||
<WorkSection>
|
<WorkSection>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,13 @@ import arrowRight from '@iconify/icons-ic/baseline-keyboard-arrow-right';
|
||||||
import edit from '@iconify/icons-ic/baseline-edit';
|
import edit from '@iconify/icons-ic/baseline-edit';
|
||||||
import checkCircle from '@iconify/icons-ic/round-check-circle';
|
import checkCircle from '@iconify/icons-ic/round-check-circle';
|
||||||
import cancel from '@iconify/icons-ic/baseline-cancel';
|
import cancel from '@iconify/icons-ic/baseline-cancel';
|
||||||
|
import userSearch from '@iconify/icons-ic/baseline-person-search';
|
||||||
import admin from '@iconify/icons-ic/baseline-admin-panel-settings';
|
import admin from '@iconify/icons-ic/baseline-admin-panel-settings';
|
||||||
import noAdmin from '@iconify/icons-ic/baseline-remove-moderator'
|
import noAdmin from '@iconify/icons-ic/baseline-remove-moderator'
|
||||||
import refresh from '@iconify/icons-ic/baseline-refresh';
|
import refresh from '@iconify/icons-ic/baseline-refresh';
|
||||||
import close from '@iconify/icons-ic/round-close';
|
import close from '@iconify/icons-ic/round-close';
|
||||||
|
import wifiFind from '@iconify/icons-ic/baseline-wifi-find';
|
||||||
|
import noteEdit from '@iconify/icons-ph/note-pencil';
|
||||||
|
|
||||||
export const ICONS = {
|
export const ICONS = {
|
||||||
[buttonNames[1]]: exitToApp,
|
[buttonNames[1]]: exitToApp,
|
||||||
|
|
@ -30,8 +33,10 @@ export const ICONS = {
|
||||||
[buttonNames[10]]: admin,
|
[buttonNames[10]]: admin,
|
||||||
[buttonNames[11]]: refresh,
|
[buttonNames[11]]: refresh,
|
||||||
[buttonNames[12]]: close,
|
[buttonNames[12]]: close,
|
||||||
[buttonNames[13]]: noAdmin
|
[buttonNames[13]]: noAdmin,
|
||||||
|
[buttonNames[14]]: userSearch,
|
||||||
|
[buttonNames[15]]: wifiFind,
|
||||||
|
[buttonNames[16]]: noteEdit,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type IconName = keyof typeof ICONS;
|
type IconName = keyof typeof ICONS;
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,8 @@ export const buttonNames: names ={
|
||||||
10: 'admin',
|
10: 'admin',
|
||||||
11: 'reset',
|
11: 'reset',
|
||||||
12: 'close',
|
12: 'close',
|
||||||
13: 'noAdmin'
|
13: 'noAdmin',
|
||||||
|
14: 'userSearch',
|
||||||
|
15: 'wifiFind',
|
||||||
|
16: 'noteEdit'
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
const baseTheme = {
|
const baseTheme = {
|
||||||
accent: '#007ACC',
|
accent: '#007ACC',
|
||||||
|
successColor: "#4CAf50",
|
||||||
|
errorColor: "#F44336",
|
||||||
accentHover: '#4A90E2',
|
accentHover: '#4A90E2',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9,6 +11,10 @@ export const light = {
|
||||||
secondary: '#EDEDED',
|
secondary: '#EDEDED',
|
||||||
background: '#E8E8E8',
|
background: '#E8E8E8',
|
||||||
textColor: '#000000',
|
textColor: '#000000',
|
||||||
|
borderColor: '#404040',
|
||||||
|
scannerColor: '#007ACC',
|
||||||
|
offlineColor: '#ababab',
|
||||||
|
hoverBtn: '#d6d6d6',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dark = {
|
export const dark = {
|
||||||
|
|
@ -17,4 +23,8 @@ export const dark = {
|
||||||
secondary: '#252525',
|
secondary: '#252525',
|
||||||
background: '#3A3A3A',
|
background: '#3A3A3A',
|
||||||
textColor: '#FFFFFF',
|
textColor: '#FFFFFF',
|
||||||
|
borderColor: '#b6b6b6',
|
||||||
|
scannerColor: '#0E9C9D',
|
||||||
|
offlineColor: '#656565',
|
||||||
|
hoverBtn: '#4a4a4a',
|
||||||
};
|
};
|
||||||
|
|
@ -4,5 +4,7 @@ export const workSpaceNames: names ={
|
||||||
1: 'helloWindow',
|
1: 'helloWindow',
|
||||||
2: 'aboutFromWork',
|
2: 'aboutFromWork',
|
||||||
3: 'networkInterfaces',
|
3: 'networkInterfaces',
|
||||||
4: 'aboutFromHello'
|
4: 'aboutFromHello',
|
||||||
|
5: 'userScanner',
|
||||||
|
6: 'wifiScanner',
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ interface IpAddressState {
|
||||||
export const useIpStore = create<IpAddressState>()(
|
export const useIpStore = create<IpAddressState>()(
|
||||||
persist(
|
persist(
|
||||||
(set)=>({
|
(set)=>({
|
||||||
ipAddress: 'localhost',
|
ipAddress: '192:168.1.147',
|
||||||
setIpAddress: (newIp: string) => set({ ipAddress: newIp }),
|
setIpAddress: (newIp: string) => set({ ipAddress: newIp }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
|
|
||||||
51
FRONT/BUILD/resources/app/src/store/useDeviceStore.ts
Normal file
51
FRONT/BUILD/resources/app/src/store/useDeviceStore.ts
Normal 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;
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useIpStore } from "../store";
|
import { useIpStore } from "../store";
|
||||||
import { useAdminStore } from "../store/adminStore";
|
import { useAdminStore } from "../store/adminStore";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { NetworkService } from "../api/request";
|
import { NetworkService } from "../api/request";
|
||||||
|
|
||||||
export const useCheckAdmin = () => {
|
export const useCheckAdmin = () => {
|
||||||
|
|
|
||||||
56
FRONT/README.md
Normal file
56
FRONT/README.md
Normal 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`
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# GoNets
|
# GoNets
|
||||||
|
|
||||||
Общий репозиторий для GoNets начиная с версии 0.0.2
|
Общий репозиторий для GoNets начиная с версии 0.0.1
|
||||||
1
SRV/.gitignore
vendored
1
SRV/.gitignore
vendored
|
|
@ -6,3 +6,4 @@ __pycache__
|
||||||
.wheels
|
.wheels
|
||||||
.vendor
|
.vendor
|
||||||
.python3-deb
|
.python3-deb
|
||||||
|
storage/devices/
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from api.v1 import ufw, websocket, ip, network, auth
|
from .v1 import ufw, websocket, ip, network, auth, subnet
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -8,3 +8,5 @@ router.include_router(websocket.router, tags=["WebSocket"])
|
||||||
router.include_router(ip.router, prefix="/ip", tags=["IP"])
|
router.include_router(ip.router, prefix="/ip", tags=["IP"])
|
||||||
router.include_router(network.router, prefix="/network", tags=["Network"])
|
router.include_router(network.router, prefix="/network", tags=["Network"])
|
||||||
router.include_router(auth.router, prefix="/auth", tags=["Auth"])
|
router.include_router(auth.router, prefix="/auth", tags=["Auth"])
|
||||||
|
|
||||||
|
router.include_router(subnet.router, prefix="/subnet", tags=["Subnet"])
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ async def register(user: UserCreate):
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}")
|
logger.error(f"Ошибка в register: {type(e).__name__} - {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Не удалось зарегистрировать пользователя")
|
raise HTTPException(status_code=500,
|
||||||
|
detail="Не удалось зарегистрировать пользователя")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=Token)
|
@router.post("/login", response_model=Token)
|
||||||
|
|
@ -36,13 +37,16 @@ async def login(user: UserLogin, request: Request):
|
||||||
client_id = request.client.host
|
client_id = request.client.host
|
||||||
logger.debug(f"Попытка авторизации для client_id {client_id}")
|
logger.debug(f"Попытка авторизации для client_id {client_id}")
|
||||||
result = auth_service.login(user, client_id)
|
result = auth_service.login(user, client_id)
|
||||||
logger.info(f"Успешная авторизация для client_id {client_id}, session_id: {result['session_id']}")
|
logger.info(
|
||||||
|
f"Успешная авторизация для client_id {client_id}, session_id: {result['session_id']}"
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}")
|
logger.error(f"Ошибка в login: {type(e).__name__} - {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Не удалось авторизовать пользователя")
|
raise HTTPException(status_code=500,
|
||||||
|
detail="Не удалось авторизовать пользователя")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me")
|
@router.get("/me")
|
||||||
|
|
@ -55,13 +59,15 @@ async def get_current_user(request: Request):
|
||||||
return {
|
return {
|
||||||
"user": result["user"],
|
"user": result["user"],
|
||||||
"access_token": result["access_token"],
|
"access_token": result["access_token"],
|
||||||
"token_type": "bearer"
|
"token_type": "bearer",
|
||||||
}
|
}
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
raise HTTPException(status_code=500, detail="Не удалось получить данные пользователя")
|
f"Ошибка в get_current_user: {type(e).__name__} - {str(e)}")
|
||||||
|
raise HTTPException(status_code=500,
|
||||||
|
detail="Не удалось получить данные пользователя")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/logout")
|
@router.get("/logout")
|
||||||
|
|
@ -75,7 +81,8 @@ async def logout(request: Request):
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}")
|
logger.error(f"Ошибка в logout: {type(e).__name__} - {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Не удалось выйти из системы")
|
raise HTTPException(status_code=500,
|
||||||
|
detail="Не удалось выйти из системы")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/check_json")
|
@router.get("/check_json")
|
||||||
|
|
@ -90,25 +97,34 @@ async def check_users_file():
|
||||||
return {
|
return {
|
||||||
"exists": False,
|
"exists": False,
|
||||||
"has_users": False,
|
"has_users": False,
|
||||||
"message": "Файл users.json не существует"
|
"message": "Файл users.json не существует",
|
||||||
}
|
}
|
||||||
|
|
||||||
data = storage._read_data()
|
data = storage._read_data()
|
||||||
has_users = len(data.get("users", [])) > 0
|
has_users = len(data.get("users", [])) > 0
|
||||||
if has_users:
|
if has_users:
|
||||||
logger.info(f"Файл {file_path} существует и содержит пользователей")
|
logger.info(
|
||||||
|
f"Файл {file_path} существует и содержит пользователей")
|
||||||
return {
|
return {
|
||||||
"exists": True,
|
"exists": True,
|
||||||
"has_users": True,
|
"has_users": True,
|
||||||
"message": "Файл users.json существует и содержит пользователей"
|
"message":
|
||||||
|
"Файл users.json существует и содержит пользователей",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
logger.info(f"Файл {file_path} существует, но не содержит пользователей")
|
logger.info(
|
||||||
|
f"Файл {file_path} существует, но не содержит пользователей")
|
||||||
return {
|
return {
|
||||||
"exists": True,
|
"exists":
|
||||||
"has_users": False,
|
True,
|
||||||
"message": "Файл users.json существует, но не содержит пользователей"
|
"has_users":
|
||||||
|
False,
|
||||||
|
"message":
|
||||||
|
"Файл users.json существует, но не содержит пользователей",
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
raise HTTPException(status_code=500, detail="Ошибка при проверке файла")
|
f"Ошибка при проверке файла users.json: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
|
raise HTTPException(status_code=500,
|
||||||
|
detail="Ошибка при проверке файла")
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ class SuccessResponse(BaseModel):
|
||||||
@router.get(
|
@router.get(
|
||||||
"/zones",
|
"/zones",
|
||||||
response_model=List[str],
|
response_model=List[str],
|
||||||
responses={500: {"model": ErrorResponse}},
|
responses={500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}},
|
||||||
summary="Get all available zones",
|
summary="Get all available zones",
|
||||||
)
|
)
|
||||||
async def get_zones() -> List[str]:
|
async def get_zones() -> List[str]:
|
||||||
|
|
@ -53,7 +55,9 @@ async def get_zones() -> List[str]:
|
||||||
@router.get(
|
@router.get(
|
||||||
"/zones/default",
|
"/zones/default",
|
||||||
response_model=str,
|
response_model=str,
|
||||||
responses={500: {"model": ErrorResponse}},
|
responses={500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}},
|
||||||
summary="Get default zone",
|
summary="Get default zone",
|
||||||
)
|
)
|
||||||
async def get_default_zone() -> str:
|
async def get_default_zone() -> str:
|
||||||
|
|
@ -75,7 +79,14 @@ async def get_default_zone() -> str:
|
||||||
@router.put(
|
@router.put(
|
||||||
"/zones/default/{zone}",
|
"/zones/default/{zone}",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
|
responses={
|
||||||
|
404: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}
|
||||||
|
},
|
||||||
summary="Set default zone",
|
summary="Set default zone",
|
||||||
)
|
)
|
||||||
async def set_default_zone(
|
async def set_default_zone(
|
||||||
|
|
@ -94,9 +105,8 @@ async def set_default_zone(
|
||||||
firewalld.set_default_zone(zone)
|
firewalld.set_default_zone(zone)
|
||||||
return SuccessResponse(message=f"Default zone set to {zone}")
|
return SuccessResponse(message=f"Default zone set to {zone}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
|
detail=f"Zone '{zone}' not found")
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
|
@ -107,10 +117,18 @@ async def set_default_zone(
|
||||||
@router.get(
|
@router.get(
|
||||||
"/zones/{zone}",
|
"/zones/{zone}",
|
||||||
response_model=FirewallZone,
|
response_model=FirewallZone,
|
||||||
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
|
responses={
|
||||||
|
404: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}
|
||||||
|
},
|
||||||
summary="Get zone configuration",
|
summary="Get zone configuration",
|
||||||
)
|
)
|
||||||
async def get_zone(zone: str = Path(..., description="Zone name")) -> FirewallZone:
|
async def get_zone(zone: str = Path(...,
|
||||||
|
description="Zone name")) -> FirewallZone:
|
||||||
"""
|
"""
|
||||||
Get detailed configuration for a specific zone.
|
Get detailed configuration for a specific zone.
|
||||||
|
|
||||||
|
|
@ -122,16 +140,13 @@ async def get_zone(zone: str = Path(..., description="Zone name")) -> FirewallZo
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
zone_info = firewalld.get_zone_settings(zone)
|
zone_info = firewalld.get_zone_settings(zone)
|
||||||
return FirewallZone(
|
return FirewallZone(name=zone,
|
||||||
name=zone,
|
|
||||||
target=FirewallTarget.DEFAULT,
|
target=FirewallTarget.DEFAULT,
|
||||||
interfaces=[],
|
interfaces=[],
|
||||||
**zone_info
|
**zone_info)
|
||||||
)
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
|
detail=f"Zone '{zone}' not found")
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
|
@ -142,10 +157,13 @@ async def get_zone(zone: str = Path(..., description="Zone name")) -> FirewallZo
|
||||||
@router.get(
|
@router.get(
|
||||||
"/zones/{zone}/services",
|
"/zones/{zone}/services",
|
||||||
response_model=List[str],
|
response_model=List[str],
|
||||||
responses={500: {"model": ErrorResponse}},
|
responses={500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}},
|
||||||
summary="Get services in zone",
|
summary="Get services in zone",
|
||||||
)
|
)
|
||||||
async def get_services(zone: str = Path(..., description="Zone name")) -> List[str]:
|
async def get_services(zone: str = Path(...,
|
||||||
|
description="Zone name")) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Get all services in a zone.
|
Get all services in a zone.
|
||||||
|
|
||||||
|
|
@ -167,7 +185,14 @@ async def get_services(zone: str = Path(..., description="Zone name")) -> List[s
|
||||||
@router.post(
|
@router.post(
|
||||||
"/zones/{zone}/services",
|
"/zones/{zone}/services",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
|
responses={
|
||||||
|
404: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}
|
||||||
|
},
|
||||||
summary="Add service to zone",
|
summary="Add service to zone",
|
||||||
)
|
)
|
||||||
async def add_service(
|
async def add_service(
|
||||||
|
|
@ -186,11 +211,11 @@ async def add_service(
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
firewalld.add_service(zone, rule.name, rule.timeout)
|
firewalld.add_service(zone, rule.name, rule.timeout)
|
||||||
return SuccessResponse(message=f"Service {rule.name} added to zone {zone}")
|
return SuccessResponse(
|
||||||
|
message=f"Service {rule.name} added to zone {zone}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
|
detail=f"Zone '{zone}' not found")
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
|
@ -201,7 +226,14 @@ async def add_service(
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/zones/{zone}/services/{service}",
|
"/zones/{zone}/services/{service}",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
|
responses={
|
||||||
|
404: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}
|
||||||
|
},
|
||||||
summary="Remove service from zone",
|
summary="Remove service from zone",
|
||||||
)
|
)
|
||||||
async def remove_service(
|
async def remove_service(
|
||||||
|
|
@ -220,7 +252,8 @@ async def remove_service(
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
firewalld.remove_service(zone, service)
|
firewalld.remove_service(zone, service)
|
||||||
return SuccessResponse(message=f"Service {service} removed from zone {zone}")
|
return SuccessResponse(
|
||||||
|
message=f"Service {service} removed from zone {zone}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
|
@ -236,10 +269,13 @@ async def remove_service(
|
||||||
@router.get(
|
@router.get(
|
||||||
"/zones/{zone}/ports",
|
"/zones/{zone}/ports",
|
||||||
response_model=List[str],
|
response_model=List[str],
|
||||||
responses={500: {"model": ErrorResponse}},
|
responses={500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}},
|
||||||
summary="Get ports in zone",
|
summary="Get ports in zone",
|
||||||
)
|
)
|
||||||
async def get_ports(zone: str = Path(..., description="Zone name")) -> List[str]:
|
async def get_ports(zone: str = Path(...,
|
||||||
|
description="Zone name")) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Get all ports in a zone.
|
Get all ports in a zone.
|
||||||
|
|
||||||
|
|
@ -262,9 +298,15 @@ async def get_ports(zone: str = Path(..., description="Zone name")) -> List[str]
|
||||||
"/zones/{zone}/ports",
|
"/zones/{zone}/ports",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
responses={
|
responses={
|
||||||
400: {"model": ErrorResponse},
|
400: {
|
||||||
404: {"model": ErrorResponse},
|
"model": ErrorResponse
|
||||||
500: {"model": ErrorResponse},
|
},
|
||||||
|
404: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
},
|
||||||
},
|
},
|
||||||
summary="Add port to zone",
|
summary="Add port to zone",
|
||||||
)
|
)
|
||||||
|
|
@ -285,10 +327,11 @@ async def add_port(
|
||||||
try:
|
try:
|
||||||
firewalld.add_port(zone, rule.port, rule.protocol.value, rule.timeout)
|
firewalld.add_port(zone, rule.port, rule.protocol.value, rule.timeout)
|
||||||
return SuccessResponse(
|
return SuccessResponse(
|
||||||
message=f"Port {rule.port}/{rule.protocol.value} added to zone {zone}"
|
message=
|
||||||
)
|
f"Port {rule.port}/{rule.protocol.value} added to zone {zone}")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
|
@ -299,7 +342,14 @@ async def add_port(
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/zones/{zone}/ports",
|
"/zones/{zone}/ports",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
|
responses={
|
||||||
|
404: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}
|
||||||
|
},
|
||||||
summary="Remove port from zone",
|
summary="Remove port from zone",
|
||||||
)
|
)
|
||||||
async def remove_port(
|
async def remove_port(
|
||||||
|
|
@ -319,12 +369,11 @@ async def remove_port(
|
||||||
try:
|
try:
|
||||||
firewalld.remove_port(zone, rule.port, rule.protocol.value)
|
firewalld.remove_port(zone, rule.port, rule.protocol.value)
|
||||||
return SuccessResponse(
|
return SuccessResponse(
|
||||||
message=f"Port {rule.port}/{rule.protocol.value} removed from zone {zone}"
|
message=
|
||||||
)
|
f"Port {rule.port}/{rule.protocol.value} removed from zone {zone}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
|
detail=f"Zone '{zone}' not found")
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
|
@ -335,10 +384,13 @@ async def remove_port(
|
||||||
@router.get(
|
@router.get(
|
||||||
"/zones/{zone}/sources",
|
"/zones/{zone}/sources",
|
||||||
response_model=List[str],
|
response_model=List[str],
|
||||||
responses={500: {"model": ErrorResponse}},
|
responses={500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}},
|
||||||
summary="Get sources in zone",
|
summary="Get sources in zone",
|
||||||
)
|
)
|
||||||
async def get_sources(zone: str = Path(..., description="Zone name")) -> List[str]:
|
async def get_sources(zone: str = Path(...,
|
||||||
|
description="Zone name")) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Get all sources in a zone.
|
Get all sources in a zone.
|
||||||
|
|
||||||
|
|
@ -360,7 +412,14 @@ async def get_sources(zone: str = Path(..., description="Zone name")) -> List[st
|
||||||
@router.post(
|
@router.post(
|
||||||
"/zones/{zone}/sources",
|
"/zones/{zone}/sources",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
|
responses={
|
||||||
|
404: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}
|
||||||
|
},
|
||||||
summary="Add source to zone",
|
summary="Add source to zone",
|
||||||
)
|
)
|
||||||
async def add_source(
|
async def add_source(
|
||||||
|
|
@ -379,11 +438,11 @@ async def add_source(
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
firewalld.add_source(zone, rule.address)
|
firewalld.add_source(zone, rule.address)
|
||||||
return SuccessResponse(message=f"Source {rule.address} added to zone {zone}")
|
return SuccessResponse(
|
||||||
|
message=f"Source {rule.address} added to zone {zone}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"Zone '{zone}' not found"
|
detail=f"Zone '{zone}' not found")
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
|
@ -394,7 +453,14 @@ async def add_source(
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/zones/{zone}/sources/{source}",
|
"/zones/{zone}/sources/{source}",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
responses={404: {"model": ErrorResponse}, 500: {"model": ErrorResponse}},
|
responses={
|
||||||
|
404: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}
|
||||||
|
},
|
||||||
summary="Remove source from zone",
|
summary="Remove source from zone",
|
||||||
)
|
)
|
||||||
async def remove_source(
|
async def remove_source(
|
||||||
|
|
@ -413,7 +479,8 @@ async def remove_source(
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
firewalld.remove_source(zone, source)
|
firewalld.remove_source(zone, source)
|
||||||
return SuccessResponse(message=f"Source {source} removed from zone {zone}")
|
return SuccessResponse(
|
||||||
|
message=f"Source {source} removed from zone {zone}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
|
@ -429,7 +496,9 @@ async def remove_source(
|
||||||
@router.post(
|
@router.post(
|
||||||
"/reload",
|
"/reload",
|
||||||
response_model=SuccessResponse,
|
response_model=SuccessResponse,
|
||||||
responses={500: {"model": ErrorResponse}},
|
responses={500: {
|
||||||
|
"model": ErrorResponse
|
||||||
|
}},
|
||||||
summary="Reload firewall rules",
|
summary="Reload firewall rules",
|
||||||
)
|
)
|
||||||
async def reload_firewall() -> SuccessResponse:
|
async def reload_firewall() -> SuccessResponse:
|
||||||
|
|
|
||||||
|
|
@ -5,33 +5,52 @@ from services.ip_service import IPService
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
ip_service = IPService()
|
ip_service = IPService()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/check_ip")
|
@router.post("/check_ip")
|
||||||
async def check_ip_config(data: Dict[str, Any] = Body(...)):
|
async def check_ip_config(data: Dict[str, Any] = Body(...)):
|
||||||
""" Проверяет доступность IP-адреса """
|
"""Проверяет доступность IP-адреса"""
|
||||||
try:
|
try:
|
||||||
result = await ip_service.check_ip_config(data)
|
result = await ip_service.check_ip_config(data)
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Ошибка проверки IP конфигурации: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/apply_ip")
|
@router.post("/apply_ip")
|
||||||
async def apply_ip_config(data: Dict[str, Any] = Body(...)):
|
async def apply_ip_config(data: Dict[str, Any] = Body(...)):
|
||||||
""" Применяет проверенные IP-настройки """
|
"""Применяет проверенные IP-настройки"""
|
||||||
try:
|
try:
|
||||||
await ip_service.apply_ip_config(data)
|
await ip_service.apply_ip_config(data)
|
||||||
return {"success": True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены"}
|
return {
|
||||||
|
"success":
|
||||||
|
True,
|
||||||
|
"message":
|
||||||
|
f"Настройки сетевого профиля для {data['connection_id']} применены",
|
||||||
|
}
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Ошибка проверки IP конфигурации: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/save_ip")
|
@router.post("/save_ip")
|
||||||
async def save_ip_config(data: Dict[str, Any] = Body(...)):
|
async def save_ip_config(data: Dict[str, Any] = Body(...)):
|
||||||
""" Применяет проверенные IP-настройки (Нужен для того, чтобы откатить настройки сетевого профиля обратно) """
|
"""Применяет проверенные IP-настройки (Нужен для того, чтобы откатить настройки сетевого профиля обратно)"""
|
||||||
try:
|
try:
|
||||||
await ip_service.apply_ip_config(data)
|
await ip_service.apply_ip_config(data)
|
||||||
return {"success": True, "message": f"Настройки сетевого профиля для {data['connection_id']} применены"}
|
return {
|
||||||
|
"success":
|
||||||
|
True,
|
||||||
|
"message":
|
||||||
|
f"Настройки сетевого профиля для {data['connection_id']} применены",
|
||||||
|
}
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=404, detail=str(e))
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Ошибка проверки IP конфигурации: {str(e)}")
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Ошибка проверки IP конфигурации: {str(e)}")
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,13 @@ import logging
|
||||||
from typing import Dict, Tuple, List, Union
|
from typing import Dict, Tuple, List, Union
|
||||||
|
|
||||||
from services.network_service import NetworkService
|
from services.network_service import NetworkService
|
||||||
from models.network import NetworkInterfacesContainer, Activation, Deactivation, NetworkInterface, WiFiDevice
|
from models.network import (
|
||||||
|
NetworkInterfacesContainer,
|
||||||
|
Activation,
|
||||||
|
Deactivation,
|
||||||
|
NetworkInterface,
|
||||||
|
WiFiDevice,
|
||||||
|
)
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -19,19 +25,29 @@ class ContainerPositionManager:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.positions: Dict[str, Dict[str, Union[int, bool]]] = {
|
self.positions: Dict[str, Dict[str, Union[int, bool]]] = {
|
||||||
'activation': {'x': 0, 'y': 0, 'isOpen': False},
|
"activation": {
|
||||||
'deactivation': {'x': 0, 'y': 0, 'isOpen': False}
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"isOpen": False
|
||||||
|
},
|
||||||
|
"deactivation": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"isOpen": False
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_position_and_state(self, container_name: str) -> Tuple[int, int, bool]:
|
def get_position_and_state(self,
|
||||||
|
container_name: str) -> Tuple[int, int, bool]:
|
||||||
container = self.positions.get(container_name)
|
container = self.positions.get(container_name)
|
||||||
if not container:
|
if not container:
|
||||||
raise ValueError(f"Неверное имя контейнера: {container_name}")
|
raise ValueError(f"Неверное имя контейнера: {container_name}")
|
||||||
return container['x'], container['y'], container['isOpen']
|
return container["x"], container["y"], container["isOpen"]
|
||||||
|
|
||||||
def update_position(self, container_id: int, x: int, y: int, isOpen: bool) -> None:
|
def update_position(self, container_id: int, x: int, y: int,
|
||||||
container_name = 'activation' if container_id == 1 else 'deactivation'
|
isOpen: bool) -> None:
|
||||||
self.positions[container_name] = {'x': x, 'y': y, 'isOpen': isOpen}
|
container_name = "activation" if container_id == 1 else "deactivation"
|
||||||
|
self.positions[container_name] = {"x": x, "y": y, "isOpen": isOpen}
|
||||||
|
|
||||||
|
|
||||||
container_manager = ContainerPositionManager()
|
container_manager = ContainerPositionManager()
|
||||||
|
|
@ -45,44 +61,50 @@ async def get_network_data() -> Dict[str, Dict]:
|
||||||
try:
|
try:
|
||||||
network_data = await network_service.get_network_interfaces_container()
|
network_data = await network_service.get_network_interfaces_container()
|
||||||
|
|
||||||
activation_interfaces, deactivation_interfaces = _split_interfaces(network_data.interfaces)
|
activation_interfaces, deactivation_interfaces = _split_interfaces(
|
||||||
|
network_data.interfaces)
|
||||||
|
|
||||||
activation_x, activation_y, activation_is_open = container_manager.get_position_and_state('activation')
|
activation_x, activation_y, activation_is_open = (
|
||||||
deactivation_x, deactivation_y, deactivation_is_open = container_manager.get_position_and_state('deactivation')
|
container_manager.get_position_and_state("activation"))
|
||||||
|
deactivation_x, deactivation_y, deactivation_is_open = (
|
||||||
|
container_manager.get_position_and_state("deactivation"))
|
||||||
|
|
||||||
activation = Activation(
|
activation = Activation(
|
||||||
id=1,
|
id=1,
|
||||||
interfaces=activation_interfaces,
|
interfaces=activation_interfaces,
|
||||||
container_x=activation_x,
|
container_x=activation_x,
|
||||||
container_y=activation_y,
|
container_y=activation_y,
|
||||||
isOpen=activation_is_open
|
isOpen=activation_is_open,
|
||||||
)
|
)
|
||||||
deactivation = Deactivation(
|
deactivation = Deactivation(
|
||||||
id=2,
|
id=2,
|
||||||
interfaces=deactivation_interfaces,
|
interfaces=deactivation_interfaces,
|
||||||
container_x=deactivation_x,
|
container_x=deactivation_x,
|
||||||
container_y=deactivation_y,
|
container_y=deactivation_y,
|
||||||
isOpen=deactivation_is_open
|
isOpen=deactivation_is_open,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"activation": activation.model_dump(),
|
"activation": activation.model_dump(),
|
||||||
"deactivation": deactivation.model_dump()
|
"deactivation": deactivation.model_dump(),
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка в get_network_data: {e}")
|
logger.error(f"Ошибка в get_network_data: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Не удалось получить сетевые данные")
|
raise HTTPException(status_code=500,
|
||||||
|
detail="Не удалось получить сетевые данные")
|
||||||
|
|
||||||
|
|
||||||
def _split_interfaces(interfaces: List[Union[NetworkInterface, WiFiDevice]]) -> Tuple[
|
def _split_interfaces(
|
||||||
List[Union[NetworkInterface, WiFiDevice]], List[Union[NetworkInterface, WiFiDevice]]]:
|
interfaces: List[Union[NetworkInterface, WiFiDevice]],
|
||||||
|
) -> Tuple[List[Union[NetworkInterface, WiFiDevice]], List[Union[
|
||||||
|
NetworkInterface, WiFiDevice]]]:
|
||||||
"""
|
"""
|
||||||
Разделяет интерфейсы на активные и неактивные
|
Разделяет интерфейсы на активные и неактивные
|
||||||
"""
|
"""
|
||||||
activation_interfaces = []
|
activation_interfaces = []
|
||||||
deactivation_interfaces = []
|
deactivation_interfaces = []
|
||||||
for interface in interfaces:
|
for interface in interfaces:
|
||||||
if interface.device_state == '100':
|
if interface.device_state == "100":
|
||||||
activation_interfaces.append(interface)
|
activation_interfaces.append(interface)
|
||||||
else:
|
else:
|
||||||
deactivation_interfaces.append(interface)
|
deactivation_interfaces.append(interface)
|
||||||
|
|
@ -90,15 +112,20 @@ def _split_interfaces(interfaces: List[Union[NetworkInterface, WiFiDevice]]) ->
|
||||||
|
|
||||||
|
|
||||||
@router.post("/update_container_position")
|
@router.post("/update_container_position")
|
||||||
async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...)):
|
async def update_container_position(data: Dict[str, Union[int,
|
||||||
|
bool]] = Body(...)):
|
||||||
"""
|
"""
|
||||||
Обновляет позицию и состояние контейнера
|
Обновляет позицию и состояние контейнера
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
|
||||||
required_keys = ["container_id", "container_x", "container_y", "isOpen"]
|
required_keys = [
|
||||||
|
"container_id", "container_x", "container_y", "isOpen"
|
||||||
|
]
|
||||||
if not all(key in data for key in required_keys):
|
if not all(key in data for key in required_keys):
|
||||||
raise ValueError("Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen")
|
raise ValueError(
|
||||||
|
"Отсутствуют обязательные поля: container_id, container_x, container_y, isOpen"
|
||||||
|
)
|
||||||
|
|
||||||
container_id = data["container_id"]
|
container_id = data["container_id"]
|
||||||
container_x = data["container_x"]
|
container_x = data["container_x"]
|
||||||
|
|
@ -106,16 +133,24 @@ async def update_container_position(data: Dict[str, Union[int, bool]] = Body(...
|
||||||
is_open = data["isOpen"]
|
is_open = data["isOpen"]
|
||||||
|
|
||||||
# Проверка типов
|
# Проверка типов
|
||||||
if not isinstance(container_id, int) or not isinstance(container_x, int) or not isinstance(container_y,
|
if (not isinstance(container_id, int)
|
||||||
int) or not isinstance(
|
or not isinstance(container_x, int)
|
||||||
is_open, bool):
|
or not isinstance(container_y, int)
|
||||||
|
or not isinstance(is_open, bool)):
|
||||||
raise ValueError("Неверный тип данных в запросе")
|
raise ValueError("Неверный тип данных в запросе")
|
||||||
|
|
||||||
container_manager.update_position(container_id, container_x, container_y, is_open)
|
container_manager.update_position(container_id, container_x,
|
||||||
return {"success": True, "message": "Позиция и состояние контейнера обновлены"}
|
container_y, is_open)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Позиция и состояние контейнера обновлены"
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка обновления позиции контейнера: {e}")
|
logger.error(f"Ошибка обновления позиции контейнера: {e}")
|
||||||
raise HTTPException(status_code=400, detail=f"Не удалось обновить положение контейнера: {str(e)}")
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"Не удалось обновить положение контейнера: {str(e)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/get_hostname")
|
@router.get("/get_hostname")
|
||||||
|
|
@ -125,4 +160,5 @@ async def get_hostname():
|
||||||
return hostname_info
|
return hostname_info
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка в get_hostname: {e}")
|
logger.error(f"Ошибка в get_hostname: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Не удалось получить имя хоста")
|
raise HTTPException(status_code=500,
|
||||||
|
detail="Не удалось получить имя хоста")
|
||||||
|
|
|
||||||
159
SRV/api/v1/subnet.py
Normal file
159
SRV/api/v1/subnet.py
Normal 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}
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from services.ufw_service import UFWService
|
|
||||||
from models.ufw import UFWRulesResponse, UFWStatus, UFWRule
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from models.ufw import UFWRulesResponse, UFWStatus, UFWRule
|
||||||
|
from services.ufw_service import UFWService
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class RuleNumbers(BaseModel):
|
class RuleNumbers(BaseModel):
|
||||||
numbers: List[int] = Field(..., description="Список номеров правил для удаления")
|
numbers: List[int] = Field(
|
||||||
|
..., description="Список номеров правил для удаления")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/rules", response_model=UFWRulesResponse)
|
@router.get("/rules", response_model=UFWRulesResponse)
|
||||||
async def get_ufw_rules():
|
async def get_ufw_rules():
|
||||||
|
|
@ -20,6 +24,7 @@ async def get_ufw_rules():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/status", response_model=UFWStatus)
|
@router.get("/status", response_model=UFWStatus)
|
||||||
async def get_ufw_status():
|
async def get_ufw_status():
|
||||||
"""
|
"""
|
||||||
|
|
@ -31,6 +36,7 @@ async def get_ufw_status():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/enable")
|
@router.post("/enable")
|
||||||
async def enable_ufw():
|
async def enable_ufw():
|
||||||
"""
|
"""
|
||||||
|
|
@ -42,10 +48,12 @@ async def enable_ufw():
|
||||||
if success:
|
if success:
|
||||||
return {"status": "success", "message": "UFW успешно включен"}
|
return {"status": "success", "message": "UFW успешно включен"}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail="Не удалось включить UFW")
|
raise HTTPException(status_code=500,
|
||||||
|
detail="Не удалось включить UFW")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/disable")
|
@router.post("/disable")
|
||||||
async def disable_ufw():
|
async def disable_ufw():
|
||||||
"""
|
"""
|
||||||
|
|
@ -57,10 +65,12 @@ async def disable_ufw():
|
||||||
if success:
|
if success:
|
||||||
return {"status": "success", "message": "UFW успешно выключен"}
|
return {"status": "success", "message": "UFW успешно выключен"}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail="Не удалось выключить UFW")
|
raise HTTPException(status_code=500,
|
||||||
|
detail="Не удалось выключить UFW")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/rules/in")
|
@router.post("/rules/in")
|
||||||
async def add_ufw_in_rules(rules: List[UFWRule]):
|
async def add_ufw_in_rules(rules: List[UFWRule]):
|
||||||
"""
|
"""
|
||||||
|
|
@ -73,23 +83,29 @@ async def add_ufw_in_rules(rules: List[UFWRule]):
|
||||||
if not status.enabled:
|
if not status.enabled:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="UFW отключен. Включите UFW перед добавлением правил."
|
detail="UFW отключен. Включите UFW перед добавлением правил.",
|
||||||
)
|
)
|
||||||
|
|
||||||
result = ufw_service.add_rules(rules)
|
result = ufw_service.add_rules(rules)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
successful_rules = len([r for r in result["results"] if r["success"]])
|
successful_rules = len(
|
||||||
|
[r for r in result["results"] if r["success"]])
|
||||||
total_rules = len(result["results"])
|
total_rules = len(result["results"])
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": f"Добавлено {successful_rules} из {total_rules} правил",
|
"message":
|
||||||
"details": result
|
f"Добавлено {successful_rules} из {total_rules} правил",
|
||||||
|
"details": result,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail=result.get("error", "Не удалось добавить правила"))
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=result.get("error", "Не удалось добавить правила"),
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/rules/out")
|
@router.post("/rules/out")
|
||||||
async def add_ufw_out_rules(rules: List[UFWRule]):
|
async def add_ufw_out_rules(rules: List[UFWRule]):
|
||||||
"""
|
"""
|
||||||
|
|
@ -102,17 +118,23 @@ async def add_ufw_out_rules(rules: List[UFWRule]):
|
||||||
if not status.enabled:
|
if not status.enabled:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="UFW отключен. Включите UFW перед добавлением правил."
|
detail="UFW отключен. Включите UFW перед добавлением правил.",
|
||||||
)
|
)
|
||||||
|
|
||||||
success = ufw_service.add_ufw_out_rules(rules)
|
success = ufw_service.add_ufw_out_rules(rules)
|
||||||
if success:
|
if success:
|
||||||
return {"status": "success", "message": "Исходящие правила UFW успешно добавлены"}
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Исходящие правила UFW успешно добавлены",
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail="Не удалось добавить исходящие правила UFW")
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Не удалось добавить исходящие правила UFW")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/rules")
|
@router.delete("/rules")
|
||||||
async def delete_ufw_rules(rule_numbers: RuleNumbers):
|
async def delete_ufw_rules(rule_numbers: RuleNumbers):
|
||||||
"""
|
"""
|
||||||
|
|
@ -124,8 +146,9 @@ async def delete_ufw_rules(rule_numbers: RuleNumbers):
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": f"Удалено {result['successful']} из {result['total']} правил",
|
"message":
|
||||||
"details": result
|
f"Удалено {result['successful']} из {result['total']} правил",
|
||||||
|
"details": result,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=500, detail=result["error"])
|
raise HTTPException(status_code=500, detail=result["error"])
|
||||||
|
|
|
||||||
|
|
@ -12,25 +12,27 @@ router = APIRouter()
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class WebSocketManager:
|
class WebSocketManager:
|
||||||
"""
|
"""
|
||||||
Класс для управления подключениями к WebSocket-серверу
|
Класс для управления подключениями к WebSocket-серверу
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
""" Инициализация менеджера подключений к WebSocket-серверу """
|
"""Инициализация менеджера подключений к WebSocket-серверу"""
|
||||||
self.active_connections: Set[WebSocket] = set()
|
self.active_connections: Set[WebSocket] = set()
|
||||||
|
|
||||||
async def connect(self, websocket: WebSocket):
|
async def connect(self, websocket: WebSocket):
|
||||||
""" Подключение клиента к WebSocket-серверу """
|
"""Подключение клиента к WebSocket-серверу"""
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
self.active_connections.add(websocket)
|
self.active_connections.add(websocket)
|
||||||
|
|
||||||
def disconnect(self, websocket: WebSocket):
|
def disconnect(self, websocket: WebSocket):
|
||||||
""" Отключение клиента от WebSocket-серверу """
|
"""Отключение клиента от WebSocket-серверу"""
|
||||||
self.active_connections.remove(websocket)
|
self.active_connections.remove(websocket)
|
||||||
|
|
||||||
async def broadcast(self, message: dict):
|
async def broadcast(self, message: dict):
|
||||||
""" Трансляция сообщений всем подключенным клиентам """
|
"""Трансляция сообщений всем подключенным клиентам"""
|
||||||
for connection in list(self.active_connections):
|
for connection in list(self.active_connections):
|
||||||
try:
|
try:
|
||||||
if connection.client_state == WebSocketState.CONNECTED:
|
if connection.client_state == WebSocketState.CONNECTED:
|
||||||
|
|
@ -41,18 +43,21 @@ class WebSocketManager:
|
||||||
logger.error(f"Сообщение об ошибке трансляции: {e}")
|
logger.error(f"Сообщение об ошибке трансляции: {e}")
|
||||||
self.disconnect(connection)
|
self.disconnect(connection)
|
||||||
|
|
||||||
|
|
||||||
manager = WebSocketManager()
|
manager = WebSocketManager()
|
||||||
|
|
||||||
|
|
||||||
@router.websocket("/api/v1")
|
@router.websocket("/api/v1")
|
||||||
async def websocket_endpoint(websocket: WebSocket):
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
""" Обработчик WebSocket-соединения """
|
"""Обработчик WebSocket-соединения"""
|
||||||
await manager.connect(websocket)
|
await manager.connect(websocket)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
data = await websocket.receive_json()
|
data = await websocket.receive_json()
|
||||||
logger.info(f"Полученные данные: {data}")
|
logger.info(f"Полученные данные: {data}")
|
||||||
|
|
||||||
if all(key in data for key in ["container_id", "container_x", "container_y"]):
|
if all(key in data
|
||||||
|
for key in ["container_id", "container_x", "container_y"]):
|
||||||
await update_container_position(data)
|
await update_container_position(data)
|
||||||
|
|
||||||
network_data = await get_network_data()
|
network_data = await get_network_data()
|
||||||
|
|
@ -69,18 +74,22 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||||
if websocket.client_state != WebSocketState.DISCONNECTED:
|
if websocket.client_state != WebSocketState.DISCONNECTED:
|
||||||
await websocket.close()
|
await websocket.close()
|
||||||
|
|
||||||
|
|
||||||
@router.on_event("startup")
|
@router.on_event("startup")
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
""" Запуск таска для трансляции сетевых данных """
|
"""Запуск таска для трансляции сетевых данных"""
|
||||||
asyncio.create_task(broadcast_data())
|
asyncio.create_task(broadcast_data())
|
||||||
|
|
||||||
|
|
||||||
async def broadcast_data():
|
async def broadcast_data():
|
||||||
""" Таск для трансляции сетевых данных """
|
"""Таск для трансляции сетевых данных"""
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
network_data = await get_network_data()
|
network_data = await get_network_data()
|
||||||
if "activation" not in network_data:
|
if "activation" not in network_data:
|
||||||
logger.error("Ошибка: Ключ 'activation' отсутствует в полученных данных сети")
|
logger.error(
|
||||||
|
"Ошибка: Ключ 'activation' отсутствует в полученных данных сети"
|
||||||
|
)
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ class CORSMiddlewareConfig:
|
||||||
"""
|
"""
|
||||||
Класс для настройки CORS политики в FastAPI
|
Класс для настройки CORS политики в FastAPI
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, app: FastAPI) -> None:
|
def __init__(self, app: FastAPI) -> None:
|
||||||
""" Инициализируем приложение FastAPI и список допустимых CORS-оригинов """
|
"""Инициализируем приложение FastAPI и список допустимых CORS-оригинов"""
|
||||||
self.app = app
|
self.app = app
|
||||||
self.origins = [
|
self.origins = [
|
||||||
"http://localhost.tiangolo.com",
|
"http://localhost.tiangolo.com",
|
||||||
|
|
@ -19,7 +20,7 @@ class CORSMiddlewareConfig:
|
||||||
]
|
]
|
||||||
|
|
||||||
def add_cors_middleware(self) -> None:
|
def add_cors_middleware(self) -> None:
|
||||||
""" Добавляем CORS в приложение FastAPI """
|
"""Добавляем CORS в приложение FastAPI"""
|
||||||
self.app.add_middleware(
|
self.app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=self.origins,
|
allow_origins=self.origins,
|
||||||
|
|
|
||||||
|
|
@ -7,29 +7,33 @@ import logging
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def init_config():
|
def init_config():
|
||||||
"""Создаёт конфигурационный файл с SECRET_KEY"""
|
"""Создаёт конфигурационный файл с SECRET_KEY"""
|
||||||
config_path = "/etc/gonets/config.ini"
|
config_path = "/etc/gonets/config.ini"
|
||||||
config_dir = os.path.dirname(config_path)
|
config_dir = os.path.dirname(config_path)
|
||||||
|
|
||||||
if os.path.exists(config_path):
|
if os.path.exists(config_path):
|
||||||
logger.info(f"Конфигурационный файл {config_path} уже существует, пропускаем инициализацию")
|
logger.info(
|
||||||
|
f"Конфигурационный файл {config_path} уже существует, пропускаем инициализацию"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
secret_key = secrets.token_urlsafe(32)
|
secret_key = secrets.token_urlsafe(32)
|
||||||
os.makedirs(config_dir, exist_ok=True)
|
os.makedirs(config_dir, exist_ok=True)
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config["JWT"] = {
|
config["JWT"] = {"secret_key": secret_key}
|
||||||
"secret_key": secret_key
|
|
||||||
}
|
|
||||||
with open(config_path, "w") as config_file:
|
with open(config_path, "w") as config_file:
|
||||||
config.write(config_file)
|
config.write(config_file)
|
||||||
os.chmod(config_path, 0o600)
|
os.chmod(config_path, 0o600)
|
||||||
logger.info(f"Создан конфигурационный файл {config_path}")
|
logger.info(f"Создан конфигурационный файл {config_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при создании конфигурационного файла: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при создании конфигурационного файла: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
init_config()
|
init_config()
|
||||||
102
SRV/config/ports.py
Normal file
102
SRV/config/ports.py
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
|
||||||
|
class PortConfig:
|
||||||
|
"""Конфигурация портов для сетевого сканирования."""
|
||||||
|
|
||||||
|
_DEFAULT_PORTS: Dict[int, str] = {
|
||||||
|
20: "FTP (Data)",
|
||||||
|
21: "FTP (Control)",
|
||||||
|
22: "SSH",
|
||||||
|
23: "Telnet",
|
||||||
|
25: "SMTP",
|
||||||
|
53: "DNS",
|
||||||
|
67: "DHCP (Server)",
|
||||||
|
68: "DHCP (Client)",
|
||||||
|
69: "TFTP",
|
||||||
|
80: "HTTP",
|
||||||
|
110: "POP3",
|
||||||
|
123: "NTP (Network Time Protocol)",
|
||||||
|
137: "NetBIOS Name Service",
|
||||||
|
138: "NetBIOS Datagram Service",
|
||||||
|
139: "NetBIOS Session Service",
|
||||||
|
143: "IMAP",
|
||||||
|
161: "SNMP",
|
||||||
|
162: "SNMP Trap",
|
||||||
|
179: "BGP (Border Gateway Protocol)",
|
||||||
|
443: "HTTPS",
|
||||||
|
445: "SMB (Microsoft-DS)",
|
||||||
|
465: "SMTPS",
|
||||||
|
514: "Syslog (UDP)",
|
||||||
|
515: "LPD (Printer)",
|
||||||
|
587: "SMTP Submission",
|
||||||
|
631: "IPP (Printing)",
|
||||||
|
993: "IMAPS",
|
||||||
|
995: "POP3S",
|
||||||
|
# --- Базы данных ---
|
||||||
|
1433: "Microsoft SQL Server",
|
||||||
|
1521: "Oracle DB",
|
||||||
|
3306: "MySQL/MariaDB",
|
||||||
|
5432: "PostgreSQL",
|
||||||
|
6379: "Redis",
|
||||||
|
27017: "MongoDB",
|
||||||
|
# --- VPN и удалённый доступ ---
|
||||||
|
1723: "PPTP VPN",
|
||||||
|
1701: "L2TP",
|
||||||
|
500: "IPSec",
|
||||||
|
4500: "IPSec NAT-T",
|
||||||
|
1194: "OpenVPN",
|
||||||
|
3389: "RDP (Remote Desktop Protocol)",
|
||||||
|
5900: "VNC",
|
||||||
|
# --- Прокси / Веб ---
|
||||||
|
1080: "SOCKS Proxy",
|
||||||
|
8000: "HTTP-Alt",
|
||||||
|
8080: "HTTP Proxy",
|
||||||
|
8443: "HTTPS-Alt",
|
||||||
|
# --- DevOps / Monitoring / Tools ---
|
||||||
|
9000: "SonarQube / PHP-FPM",
|
||||||
|
9100: "Prometheus Node Exporter",
|
||||||
|
9200: "Elasticsearch HTTP",
|
||||||
|
9300: "Elasticsearch Cluster",
|
||||||
|
11211: "Memcached",
|
||||||
|
# --- Прочее ---
|
||||||
|
6667: "IRC",
|
||||||
|
4000: "ICQ / Custom apps",
|
||||||
|
25565: "Minecraft Server",
|
||||||
|
53: "DNS (UDP & TCP)",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, custom_ports: Dict[int, str] = None):
|
||||||
|
"""
|
||||||
|
Инициализирует конфигурацию портов.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
custom_ports: Пользовательский словарь портов для добавления или переопределения.
|
||||||
|
"""
|
||||||
|
self._ports = self._DEFAULT_PORTS.copy()
|
||||||
|
if custom_ports:
|
||||||
|
self._ports.update(custom_ports)
|
||||||
|
|
||||||
|
def get_port_name(self, port: int) -> str:
|
||||||
|
"""
|
||||||
|
Возвращает название сервиса для указанного порта.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: Номер порта.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Название сервиса или строка вида 'Port-<port>'.
|
||||||
|
"""
|
||||||
|
return self._ports.get(port, f"Port-{port}")
|
||||||
|
|
||||||
|
def get_ports(self) -> List[int]:
|
||||||
|
"""
|
||||||
|
Возвращает список всех номеров портов.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список номеров портов.
|
||||||
|
"""
|
||||||
|
return list(self._ports.keys())
|
||||||
|
|
||||||
|
|
||||||
|
port_config = PortConfig()
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
|
|
||||||
class CLIExecutor:
|
class CLIExecutor:
|
||||||
"""
|
"""
|
||||||
Класс для выполнения CLI-команд
|
Класс для выполнения CLI-команд
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def run(command: list) -> Tuple[int, str]:
|
def run(command: list) -> Tuple[int, str]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -20,6 +22,6 @@ class CLIExecutor:
|
||||||
command,
|
command,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
check=False # Не вызываем исключение при ошибке
|
check=False, # Не вызываем исключение при ошибке
|
||||||
)
|
)
|
||||||
return result.returncode, result.stdout.decode('utf-8')
|
return result.returncode, result.stdout.decode("utf-8")
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
208
SRV/docs/ufw.md
208
SRV/docs/ufw.md
|
|
@ -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": "Описание ошибки"
|
|
||||||
}
|
|
||||||
|
|
@ -19,4 +19,4 @@ network_service = NetworkService()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run("main:app", host='0.0.0.0', port=8001, reload=True, ssl_keyfile="/etc/gonets/certs/gonets.key", ssl_certfile="/etc/gonets/certs/gonets.crt")
|
uvicorn.run("main:app", host='0.0.0.0', port=8001, reload=True, ssl_keyfile="/etc/gonets/certs/gonets.key", ssl_certfile="/etc/gonets/certs/gonets.crt")
|
||||||
#uvicorn.run("main:app", host='0.0.0.0', port=8001, reload=True)
|
#uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=True)
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,61 @@
|
||||||
# models.py
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(BaseModel):
|
class UserCreate(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель созданного пользователя
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
username (str): Имя пользователя
|
||||||
|
password (str): Пароль пользователя
|
||||||
|
is_admin (bool): Административный доступ
|
||||||
|
"""
|
||||||
|
|
||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
is_admin: bool = False
|
is_admin: bool = False
|
||||||
|
|
||||||
|
|
||||||
class UserLogin(BaseModel):
|
class UserLogin(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель авторизации пользователя
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
username (str): Имя пользователя
|
||||||
|
password (str): Пароль пользователя
|
||||||
|
"""
|
||||||
|
|
||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
class UserResponse(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель проверки наличия пользователя
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
id (int): Идентификатор
|
||||||
|
username (str): Имя пользователя
|
||||||
|
is_admin (bool): Административный доступ
|
||||||
|
"""
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
username: str
|
username: str
|
||||||
is_admin: bool
|
is_admin: bool
|
||||||
|
|
||||||
|
|
||||||
class Token(BaseModel):
|
class Token(BaseModel):
|
||||||
|
"""
|
||||||
|
Токен пользователя
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
access_token (str): Токен пользователя (захэширован)
|
||||||
|
token_type (str): Тип токена
|
||||||
|
session_id (Optional[str]): Идентификатор сессии
|
||||||
|
user (Optional[UserResponse]): Пользователь на основе проверки наличия пользователя
|
||||||
|
"""
|
||||||
|
|
||||||
access_token: str
|
access_token: str
|
||||||
token_type: str
|
token_type: str
|
||||||
session_id: Optional[str] = None
|
session_id: Optional[str] = None
|
||||||
|
|
|
||||||
|
|
@ -25,42 +25,35 @@ class FirewallZone(BaseModel):
|
||||||
"""Represents a firewalld zone configuration"""
|
"""Represents a firewalld zone configuration"""
|
||||||
|
|
||||||
name: Annotated[str, StringConstraints(min_length=1)] = Field(
|
name: Annotated[str, StringConstraints(min_length=1)] = Field(
|
||||||
..., description="Name of the zone"
|
..., description="Name of the zone")
|
||||||
)
|
target: Optional[FirewallTarget] = Field(default=FirewallTarget.DEFAULT,
|
||||||
target: Optional[FirewallTarget] = Field(
|
description="Zone target type")
|
||||||
default=FirewallTarget.DEFAULT, description="Zone target type"
|
services: List[str] = Field(default_factory=list,
|
||||||
)
|
description="List of enabled services")
|
||||||
services: List[str] = Field(
|
ports: List[str] = Field(default_factory=list,
|
||||||
default_factory=list, description="List of enabled services"
|
description="List of enabled ports")
|
||||||
)
|
sources: List[str] = Field(default_factory=list,
|
||||||
ports: List[str] = Field(default_factory=list, description="List of enabled ports")
|
description="List of source addresses")
|
||||||
sources: List[str] = Field(
|
interfaces: List[str] = Field(default_factory=list,
|
||||||
default_factory=list, description="List of source addresses"
|
description="List of network interfaces")
|
||||||
)
|
|
||||||
interfaces: List[str] = Field(
|
|
||||||
default_factory=list, description="List of network interfaces"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FirewallService(BaseModel):
|
class FirewallService(BaseModel):
|
||||||
"""Represents a firewalld service configuration"""
|
"""Represents a firewalld service configuration"""
|
||||||
|
|
||||||
name: Annotated[str, StringConstraints(min_length=1)] = Field(
|
name: Annotated[str, StringConstraints(min_length=1)] = Field(
|
||||||
..., description="Name of the service"
|
..., description="Name of the service")
|
||||||
)
|
ports: List[str] = Field(default_factory=list,
|
||||||
ports: List[str] = Field(
|
description="List of ports used by the service")
|
||||||
default_factory=list, description="List of ports used by the service"
|
|
||||||
)
|
|
||||||
protocols: List[str] = Field(
|
protocols: List[str] = Field(
|
||||||
default_factory=list, description="List of protocols used by the service"
|
default_factory=list,
|
||||||
)
|
description="List of protocols used by the service")
|
||||||
modules: List[str] = Field(
|
modules: List[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
description="List of kernel modules required by the service",
|
description="List of kernel modules required by the service",
|
||||||
)
|
)
|
||||||
timeout: Optional[int] = Field(
|
timeout: Optional[int] = Field(
|
||||||
default=0, ge=0, description="Timeout in seconds (0 for permanent)"
|
default=0, ge=0, description="Timeout in seconds (0 for permanent)")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FirewallPort(BaseModel):
|
class FirewallPort(BaseModel):
|
||||||
|
|
@ -69,8 +62,7 @@ class FirewallPort(BaseModel):
|
||||||
port: int = Field(..., ge=1, le=65535, description="Port number (1-65535)")
|
port: int = Field(..., ge=1, le=65535, description="Port number (1-65535)")
|
||||||
protocol: Protocol = Field(..., description="Network protocol (tcp/udp)")
|
protocol: Protocol = Field(..., description="Network protocol (tcp/udp)")
|
||||||
timeout: Optional[int] = Field(
|
timeout: Optional[int] = Field(
|
||||||
default=0, ge=0, description="Timeout in seconds (0 for permanent)"
|
default=0, ge=0, description="Timeout in seconds (0 for permanent)")
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.port}/{self.protocol.value}"
|
return f"{self.port}/{self.protocol.value}"
|
||||||
|
|
@ -81,9 +73,11 @@ class FirewallSource(BaseModel):
|
||||||
|
|
||||||
address: Annotated[
|
address: Annotated[
|
||||||
str,
|
str,
|
||||||
StringConstraints(pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?:/[0-9]{1,2})?$"),
|
StringConstraints(
|
||||||
|
pattern=r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?:/[0-9]{1,2})?$"),
|
||||||
] = Field(..., description="IP address or network in CIDR notation")
|
] = Field(..., description="IP address or network in CIDR notation")
|
||||||
zone: Optional[str] = Field(default=None, description="Zone name for the source")
|
zone: Optional[str] = Field(default=None,
|
||||||
|
description="Zone name for the source")
|
||||||
|
|
||||||
@field_validator("address")
|
@field_validator("address")
|
||||||
def validate_ip_address(cls, v):
|
def validate_ip_address(cls, v):
|
||||||
|
|
@ -106,10 +100,10 @@ class FirewallRule(BaseModel):
|
||||||
|
|
||||||
zone: str = Field(..., description="Zone name")
|
zone: str = Field(..., description="Zone name")
|
||||||
source: Optional[FirewallSource] = Field(
|
source: Optional[FirewallSource] = Field(
|
||||||
default=None, description="Source configuration"
|
default=None, description="Source configuration")
|
||||||
)
|
port: Optional[FirewallPort] = Field(default=None,
|
||||||
port: Optional[FirewallPort] = Field(default=None, description="Port configuration")
|
description="Port configuration")
|
||||||
service: Optional[str] = Field(default=None, description="Service name")
|
service: Optional[str] = Field(default=None, description="Service name")
|
||||||
timeout: Optional[int] = Field(
|
timeout: Optional[int] = Field(default=0,
|
||||||
default=0, ge=0, description="Rule timeout in seconds"
|
ge=0,
|
||||||
)
|
description="Rule timeout in seconds")
|
||||||
|
|
|
||||||
|
|
@ -2,34 +2,87 @@ from typing import List, Optional, Union
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
# Класс для маршрутов IPv4
|
|
||||||
class IPv4Route(BaseModel):
|
class IPv4Route(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель маршрута IPv4
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
destination (str): IP-адрес назначения
|
||||||
|
prefix (int): Префикс подсети в формате CIDR
|
||||||
|
next_hop (str): Адрес следующего узла (маршрутизатор)
|
||||||
|
metric (int): Метрика маршрута
|
||||||
|
"""
|
||||||
|
|
||||||
destination: str
|
destination: str
|
||||||
prefix: int
|
prefix: int
|
||||||
next_hop: str
|
next_hop: str
|
||||||
metric: int
|
metric: int
|
||||||
|
|
||||||
# Класс для маршрутов IPv6
|
|
||||||
class IPv6Route(BaseModel):
|
class IPv6Route(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель маршрута IPv6
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
destination (str): IP-адрес назначения
|
||||||
|
prefix (int): Префикс подсети (CIDR)
|
||||||
|
next_hop (str): Адрес следующего узла (маршрутизатор)
|
||||||
|
metric (int): Метрика маршрута
|
||||||
|
"""
|
||||||
|
|
||||||
destination: str
|
destination: str
|
||||||
prefix: int
|
prefix: int
|
||||||
next_hop: str
|
next_hop: str
|
||||||
metric: int
|
metric: int
|
||||||
|
|
||||||
# Класс для индивидуальной конфигурации IP
|
|
||||||
class IndividualIPConfig(BaseModel):
|
class IndividualIPConfig(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель индивидуальной конфигурации IP-адреса
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
address (str): IP-адрес интерфейса
|
||||||
|
routes (List[Union[IPv4Route, IPv6Route]]): Список маршрутов (IPv4/IPv6)
|
||||||
|
method (Optional[str]): Метод назначения IP
|
||||||
|
gateway (str): Основной шлюз по умолчанию
|
||||||
|
dns (List[str]): Список DNS-серверов
|
||||||
|
"""
|
||||||
|
|
||||||
address: str
|
address: str
|
||||||
routes: List[Union[IPv4Route, IPv6Route]]
|
routes: List[Union[IPv4Route, IPv6Route]]
|
||||||
method: Optional[str]
|
method: Optional[str]
|
||||||
gateway: str
|
gateway: str
|
||||||
dns: List[str]
|
dns: List[str]
|
||||||
|
|
||||||
# Класс для конфигурации IP
|
|
||||||
class IPConfig(BaseModel):
|
class IPConfig(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель конфигурации IP (IPv4/IPv6)
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
configurations (List[IndividualIPConfig]): Список индивидуальных IP-конфигураций
|
||||||
|
"""
|
||||||
|
|
||||||
configurations: List[IndividualIPConfig]
|
configurations: List[IndividualIPConfig]
|
||||||
|
|
||||||
# Класс для сетевого интерфейса
|
|
||||||
class NetworkInterface(BaseModel):
|
class NetworkInterface(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель базового сетевого интерфейса
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
name (str): Название интерфейса
|
||||||
|
type (str): Тип интерфейса
|
||||||
|
mac_address (str): MAC-адрес устройства
|
||||||
|
device_state (str): Состояние устройства
|
||||||
|
active_connection (str): Название активного соединения
|
||||||
|
device_mtu (str): MTU устройства
|
||||||
|
ipv4 (IPConfig): Конфигурация IPv4
|
||||||
|
ipv6 (IPConfig): Конфигурация IPv6
|
||||||
|
container_x (int): Положение по X на холсте
|
||||||
|
container_y (int): Положение по Y на холсте
|
||||||
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
type: str
|
type: str
|
||||||
mac_address: str
|
mac_address: str
|
||||||
|
|
@ -41,8 +94,20 @@ class NetworkInterface(BaseModel):
|
||||||
container_x: int = 0
|
container_x: int = 0
|
||||||
container_y: int = 0
|
container_y: int = 0
|
||||||
|
|
||||||
# Класс для WiFi устройства
|
|
||||||
class WiFiDevice(NetworkInterface):
|
class WiFiDevice(NetworkInterface):
|
||||||
|
"""
|
||||||
|
Модель Wi-Fi устройства (наследуется от NetworkInterface)
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
ssid (Optional[str]): Название Wi-Fi сети
|
||||||
|
bssid (Optional[str]): MAC-адрес точки доступа
|
||||||
|
bitrate (Optional[int]): Скорость соединения
|
||||||
|
strength (Optional[int]): Уровень сигнала
|
||||||
|
frequency (Optional[float]): Частота
|
||||||
|
mode (Union[str, int]): Режим работы
|
||||||
|
"""
|
||||||
|
|
||||||
ssid: Optional[str] = None
|
ssid: Optional[str] = None
|
||||||
bssid: Optional[str] = None
|
bssid: Optional[str] = None
|
||||||
bitrate: Optional[int] = None
|
bitrate: Optional[int] = None
|
||||||
|
|
@ -50,19 +115,53 @@ class WiFiDevice(NetworkInterface):
|
||||||
frequency: Optional[float] = None
|
frequency: Optional[float] = None
|
||||||
mode: Union[str, int] = None
|
mode: Union[str, int] = None
|
||||||
|
|
||||||
|
|
||||||
class NetworkInterfacesContainer(BaseModel):
|
class NetworkInterfacesContainer(BaseModel):
|
||||||
|
"""
|
||||||
|
Контейнер для группы сетевых интерфейсов с координатами
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
interfaces (List[Union[NetworkInterface, WiFiDevice]]): Список интерфейсов
|
||||||
|
container_x (int): Положение по оси X
|
||||||
|
container_y (int): Положение по оси Y
|
||||||
|
"""
|
||||||
|
|
||||||
interfaces: List[Union[NetworkInterface, WiFiDevice]]
|
interfaces: List[Union[NetworkInterface, WiFiDevice]]
|
||||||
container_x: int
|
container_x: int
|
||||||
container_y: int
|
container_y: int
|
||||||
|
|
||||||
|
|
||||||
class Activation(BaseModel):
|
class Activation(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель активированных интерфейсов
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
id (int): Идентификатор ноды активированных интерфейсов
|
||||||
|
interfaces (List[Union[NetworkInterface, WiFiDevice]]): Активированные интерфейсы
|
||||||
|
container_x (int): Положение по X на холсте
|
||||||
|
container_y (int): Положение по Y на холсте
|
||||||
|
isOpen (bool): Флаг скрытия ноды на холсте
|
||||||
|
"""
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
interfaces: List[Union[NetworkInterface, WiFiDevice]]
|
interfaces: List[Union[NetworkInterface, WiFiDevice]]
|
||||||
container_x: int
|
container_x: int
|
||||||
container_y: int
|
container_y: int
|
||||||
isOpen: bool = True
|
isOpen: bool = True
|
||||||
|
|
||||||
|
|
||||||
class Deactivation(BaseModel):
|
class Deactivation(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель отключенных интерфейсов
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
id (int): Идентификатор ноды отключенных интерфейсов
|
||||||
|
interfaces (List[Union[NetworkInterface, WiFiDevice]]): Отключенные интерфейсы
|
||||||
|
container_x (int): Положение по X на холсте
|
||||||
|
container_y (int): Положение по Y на холсте
|
||||||
|
isOpen (bool): Флаг скрытия ноды на холсте
|
||||||
|
"""
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
interfaces: List[Union[NetworkInterface, WiFiDevice]]
|
interfaces: List[Union[NetworkInterface, WiFiDevice]]
|
||||||
container_x: int
|
container_x: int
|
||||||
|
|
|
||||||
32
SRV/models/request.py
Normal file
32
SRV/models/request.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class NoteRequest(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель добавления заметки к просканированному устройству
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
note (str): Заметка устройства
|
||||||
|
"""
|
||||||
|
|
||||||
|
note: str
|
||||||
|
|
||||||
|
class WifiConnectRequest(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель для подключения к точке доступа Wi-Fi
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
ssid (str): Название точки доступа Wi-Fi
|
||||||
|
password (str): Пароль точки доступа Wi-Fi
|
||||||
|
"""
|
||||||
|
ssid: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
class WifiDisconnectRequest(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель для отключения от точки доступа Wi-Fi
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
ssid (str): Название точки доступа Wi-Fi
|
||||||
|
"""
|
||||||
|
ssid: str
|
||||||
38
SRV/models/subnet.py
Normal file
38
SRV/models/subnet.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
|
||||||
|
class Device(BaseModel):
|
||||||
|
"""
|
||||||
|
Сканируемое устройство в подсети
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
ip (str): IP-адрес устройства
|
||||||
|
mac (Optional[str]): MAC-адрес устройства
|
||||||
|
hostname (Optional[str]): Имя хоста
|
||||||
|
vendor (Optional[str]): Название производителя устройства (По MAC-адресу)
|
||||||
|
open_ports (List[str]): Открытые порты устройства
|
||||||
|
status (str): Статус устройства (онлайн/оффлайн)
|
||||||
|
last_seen (Optional[str]): Последняя активность устройства
|
||||||
|
note (Optional[str]): Заметка устройства
|
||||||
|
"""
|
||||||
|
|
||||||
|
ip: str
|
||||||
|
mac: Optional[str] = None
|
||||||
|
hostname: Optional[str] = None
|
||||||
|
vendor: Optional[str] = None
|
||||||
|
open_ports: List[str] = []
|
||||||
|
status: str
|
||||||
|
last_seen: Optional[str] = None
|
||||||
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SubnetScanResult(BaseModel):
|
||||||
|
"""
|
||||||
|
Результат сканирования устройств в подсети
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
devices (List[Device]): Список просканированных устройств
|
||||||
|
"""
|
||||||
|
|
||||||
|
devices: List[Device] = []
|
||||||
|
|
@ -8,6 +8,7 @@ class Protocol(str, Enum):
|
||||||
"""
|
"""
|
||||||
Enum для сетевых протоколов UFW.
|
Enum для сетевых протоколов UFW.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TCP = "tcp"
|
TCP = "tcp"
|
||||||
UDP = "udp"
|
UDP = "udp"
|
||||||
BOTH = "both"
|
BOTH = "both"
|
||||||
|
|
@ -27,6 +28,7 @@ class Action(str, Enum):
|
||||||
DENY: Запретить трафик без уведомления
|
DENY: Запретить трафик без уведомления
|
||||||
REJECT: Запретить трафик с уведомлением отправителю
|
REJECT: Запретить трафик с уведомлением отправителю
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ALLOW = "allow"
|
ALLOW = "allow"
|
||||||
DENY = "deny"
|
DENY = "deny"
|
||||||
REJECT = "reject"
|
REJECT = "reject"
|
||||||
|
|
@ -40,12 +42,14 @@ class Direction(str, Enum):
|
||||||
IN: Входящий трафик
|
IN: Входящий трафик
|
||||||
OUT: Исходящий трафик
|
OUT: Исходящий трафик
|
||||||
"""
|
"""
|
||||||
|
|
||||||
IN = "in"
|
IN = "in"
|
||||||
OUT = "out"
|
OUT = "out"
|
||||||
|
|
||||||
|
|
||||||
class UFWRule(BaseModel):
|
class UFWRule(BaseModel):
|
||||||
"""Модель правила UFW"""
|
"""Модель правила UFW"""
|
||||||
|
|
||||||
action: Action
|
action: Action
|
||||||
direction: Direction
|
direction: Direction
|
||||||
from_ip: str
|
from_ip: str
|
||||||
|
|
@ -70,18 +74,23 @@ class UFWRule(BaseModel):
|
||||||
"""
|
"""
|
||||||
if isinstance(v, int):
|
if isinstance(v, int):
|
||||||
if not (1 <= v <= 65535):
|
if not (1 <= v <= 65535):
|
||||||
raise ValueError(f"Порт должен быть в диапазоне 1-65535, получено: {v}")
|
raise ValueError(
|
||||||
|
f"Порт должен быть в диапазоне 1-65535, получено: {v}")
|
||||||
return v
|
return v
|
||||||
elif isinstance(v, str):
|
elif isinstance(v, str):
|
||||||
try:
|
try:
|
||||||
start, end = map(int, v.split(':'))
|
start, end = map(int, v.split(":"))
|
||||||
if not (1 <= start <= end <= 65535):
|
if not (1 <= start <= end <= 65535):
|
||||||
raise ValueError
|
raise ValueError
|
||||||
return v
|
return v
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
raise ValueError(f"Некорректный диапазон портов: {v}. Формат: start:end, где 1 <= start <= end <= 65535")
|
raise ValueError(
|
||||||
|
f"Некорректный диапазон портов: {v}. Формат: start:end, где 1 <= start <= end <= 65535"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Порт должен быть числом или строкой с диапазоном, получено: {type(v)}")
|
raise ValueError(
|
||||||
|
f"Порт должен быть числом или строкой с диапазоном, получено: {type(v)}"
|
||||||
|
)
|
||||||
|
|
||||||
@validator("from_ip", "to_ip")
|
@validator("from_ip", "to_ip")
|
||||||
def validate_ip(cls, v):
|
def validate_ip(cls, v):
|
||||||
|
|
@ -103,8 +112,11 @@ class UFWRule(BaseModel):
|
||||||
from_ip = ipaddress.ip_network(values["from_ip"], strict=False)
|
from_ip = ipaddress.ip_network(values["from_ip"], strict=False)
|
||||||
to_ip = ipaddress.ip_network(v, strict=False)
|
to_ip = ipaddress.ip_network(v, strict=False)
|
||||||
|
|
||||||
if isinstance(from_ip, ipaddress.IPv6Network) != isinstance(to_ip, ipaddress.IPv6Network):
|
if isinstance(from_ip, ipaddress.IPv6Network) != isinstance(
|
||||||
raise ValueError("Несовместимые версии IP: нельзя смешивать IPv4 и IPv6 в одном правиле")
|
to_ip, ipaddress.IPv6Network):
|
||||||
|
raise ValueError(
|
||||||
|
"Несовместимые версии IP: нельзя смешивать IPv4 и IPv6 в одном правиле"
|
||||||
|
)
|
||||||
|
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
@ -139,7 +151,8 @@ class UFWRule(BaseModel):
|
||||||
cmd_parts.extend(["to", self.to_ip])
|
cmd_parts.extend(["to", self.to_ip])
|
||||||
|
|
||||||
# Добавляем порт и протокол
|
# Добавляем порт и протокол
|
||||||
port_str = str(self.port) # Работает как с числами, так и с диапазонами
|
port_str = str(
|
||||||
|
self.port) # Работает как с числами, так и с диапазонами
|
||||||
cmd_parts.extend(["port", port_str])
|
cmd_parts.extend(["port", port_str])
|
||||||
|
|
||||||
# Добавляем протокол
|
# Добавляем протокол
|
||||||
|
|
@ -183,26 +196,27 @@ class UFWRule(BaseModel):
|
||||||
|
|
||||||
match = re.match(pattern, line, re.VERBOSE | re.IGNORECASE)
|
match = re.match(pattern, line, re.VERBOSE | re.IGNORECASE)
|
||||||
if not match:
|
if not match:
|
||||||
raise ValueError(f"Невозможно разобрать строку правила UFW: {line}")
|
raise ValueError(
|
||||||
|
f"Невозможно разобрать строку правила UFW: {line}")
|
||||||
|
|
||||||
groups = match.groupdict()
|
groups = match.groupdict()
|
||||||
|
|
||||||
# Преобразуем протокол в enum
|
# Преобразуем протокол в enum
|
||||||
proto_str = groups['protocol'].lower()
|
proto_str = groups["protocol"].lower()
|
||||||
protocol = Protocol.BOTH if proto_str == "tcp/udp" else \
|
protocol = (Protocol.BOTH if proto_str == "tcp/udp" else
|
||||||
Protocol.UDP if proto_str == "udp" else Protocol.TCP
|
Protocol.UDP if proto_str == "udp" else Protocol.TCP)
|
||||||
|
|
||||||
# Преобразуем действие в enum (игнорируем регистр)
|
# Преобразуем действие в enum (игнорируем регистр)
|
||||||
action_str = groups['action'].lower()
|
action_str = groups["action"].lower()
|
||||||
action = Action.ALLOW if action_str == "allow" else \
|
action = (Action.ALLOW if action_str == "allow" else
|
||||||
Action.DENY if action_str == "deny" else Action.REJECT
|
Action.DENY if action_str == "deny" else Action.REJECT)
|
||||||
|
|
||||||
# Преобразуем направление в enum
|
# Преобразуем направление в enum
|
||||||
direction_str = groups['direction'].lower()
|
direction_str = groups["direction"].lower()
|
||||||
direction = Direction.IN if direction_str == "in" else Direction.OUT
|
direction = Direction.IN if direction_str == "in" else Direction.OUT
|
||||||
|
|
||||||
# Обрабатываем IP адреса
|
# Обрабатываем IP адреса
|
||||||
from_ip = groups['from_ip'].strip()
|
from_ip = groups["from_ip"].strip()
|
||||||
if from_ip.lower() == "anywhere":
|
if from_ip.lower() == "anywhere":
|
||||||
from_ip = "0.0.0.0/0"
|
from_ip = "0.0.0.0/0"
|
||||||
|
|
||||||
|
|
@ -210,8 +224,8 @@ class UFWRule(BaseModel):
|
||||||
to_ip = "0.0.0.0/0" # По умолчанию anywhere
|
to_ip = "0.0.0.0/0" # По умолчанию anywhere
|
||||||
|
|
||||||
# Обрабатываем порт
|
# Обрабатываем порт
|
||||||
port = groups['port']
|
port = groups["port"]
|
||||||
if ':' in port:
|
if ":" in port:
|
||||||
# Оставляем как строку для диапазона
|
# Оставляем как строку для диапазона
|
||||||
port = port.strip()
|
port = port.strip()
|
||||||
else:
|
else:
|
||||||
|
|
@ -219,7 +233,8 @@ class UFWRule(BaseModel):
|
||||||
port = int(port)
|
port = int(port)
|
||||||
|
|
||||||
# Обрабатываем комментарий
|
# Обрабатываем комментарий
|
||||||
comment = groups.get('comment', '').strip() if groups.get('comment') else None
|
comment = groups.get("comment",
|
||||||
|
"").strip() if groups.get("comment") else None
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
action=action,
|
action=action,
|
||||||
|
|
@ -228,7 +243,7 @@ class UFWRule(BaseModel):
|
||||||
to_ip=to_ip,
|
to_ip=to_ip,
|
||||||
port=port,
|
port=port,
|
||||||
protocol=protocol,
|
protocol=protocol,
|
||||||
comment=comment
|
comment=comment,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -241,8 +256,10 @@ class UFWRulesResponse(BaseModel):
|
||||||
rules: Список правил UFW
|
rules: Список правил UFW
|
||||||
total_rules: Общее количество правил
|
total_rules: Общее количество правил
|
||||||
"""
|
"""
|
||||||
|
|
||||||
enabled: bool = Field(..., description="Включен ли UFW")
|
enabled: bool = Field(..., description="Включен ли UFW")
|
||||||
rules: List[UFWRule] = Field(default_factory=list, description="Список правил UFW")
|
rules: List[UFWRule] = Field(default_factory=list,
|
||||||
|
description="Список правил UFW")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_rules(self) -> int:
|
def total_rules(self) -> int:
|
||||||
|
|
@ -259,5 +276,6 @@ class UFWStatus(BaseModel):
|
||||||
message: Сообщение о статусе
|
message: Сообщение о статусе
|
||||||
last_checked: Время последней проверки статуса
|
last_checked: Время последней проверки статуса
|
||||||
"""
|
"""
|
||||||
|
|
||||||
enabled: bool = Field(..., description="Включен ли UFW")
|
enabled: bool = Field(..., description="Включен ли UFW")
|
||||||
message: str = Field(..., description="Сообщение о статусе")
|
message: str = Field(..., description="Сообщение о статусе")
|
||||||
|
|
|
||||||
40
SRV/models/wifi.py
Normal file
40
SRV/models/wifi.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
|
||||||
|
class WifiAccessPoint(BaseModel):
|
||||||
|
"""
|
||||||
|
Модель точки доступа Wi-Fi
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
ssid (Optional[str]): Название точки доступа. Может быть скрытым
|
||||||
|
bssid (str): Уникальный идентификатор точки доступа (по типу MAC-адреса)
|
||||||
|
channel (Optional[int]): Канал, на котором работает точка доступа
|
||||||
|
signal (Optional[int]): Уровень сигнала (dBm)
|
||||||
|
encryption (Optional[str]): Тип шифрования (WPA2, WPA3, открытая и т.д.)
|
||||||
|
frequency (Optional[str]): Частота вещания (2.4GHz/5GHz)
|
||||||
|
vendor (Optional[str]): Производитель точки доступа (определяется по MAC-адресу)
|
||||||
|
hidden (bool): Флаг скрытия названия точки доступа (ssid)
|
||||||
|
note (Optional[str]): Заметка для точки доступа
|
||||||
|
"""
|
||||||
|
|
||||||
|
ssid: Optional[str] = None
|
||||||
|
bssid: str
|
||||||
|
channel: Optional[int] = None
|
||||||
|
signal: Optional[int] = None
|
||||||
|
encryption: Optional[str] = None
|
||||||
|
frequency: Optional[str] = None
|
||||||
|
vendor: Optional[str] = None
|
||||||
|
hidden: bool = False
|
||||||
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class WifiScanResult(BaseModel):
|
||||||
|
"""
|
||||||
|
Результат сканирования Wi-Fi
|
||||||
|
|
||||||
|
Атрибуты:
|
||||||
|
access_points (List[WifiAccessPoint]): Список найденных Wi-Fi точек доступа
|
||||||
|
"""
|
||||||
|
|
||||||
|
access_points: List[WifiAccessPoint]
|
||||||
669
SRV/poetry.lock
generated
669
SRV/poetry.lock
generated
|
|
@ -1,4 +1,151 @@
|
||||||
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiofiles"
|
||||||
|
version = "24.1.0"
|
||||||
|
description = "File support for asyncio."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"},
|
||||||
|
{file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiohappyeyeballs"
|
||||||
|
version = "2.6.1"
|
||||||
|
description = "Happy Eyeballs for asyncio"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"},
|
||||||
|
{file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiohttp"
|
||||||
|
version = "3.12.7"
|
||||||
|
description = "Async http client/server framework (asyncio)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4486f399573c94b223411bc5686b5cdc661f4dd67daece800662356e46b3a2b5"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67759acb11673c1b976a516f2d69a73433aad70ed04e44ce79eaf0e58219535e"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8de89889df856101176ccaf570075b73b62ea9d86e11e642d0f20ecd62a34ce8"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53ae6140303ab04a7203f8fcb9ca5b2c5abea46e12e8d6f65575d0f197812e22"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0d6575df942e7991e1450b731ad9a5726a1116668471a07d749bd9b2cb1f30a7"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56d0f622b3595f3aeaefd07aca9d425748fc8bf5e9e502f75a2dad15f2b875b2"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c7c848ad08722bfc9da0b9fe5f44cde4fa6499d34ece11462c5b7b1f75a5a1b"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b56a4fb31fe82ee58cd8cc157e4fc58d19fba2580b46a62fe7808353bb9b82df"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbdb60ab46f696a5e52d98a830b11c034d601bbe2496a82a19d94268257ac63b"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8eb5d60790ca3563a376ef297dfac3c4c5ec7a7e180b9fe0314f238813fd2ab0"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f1a478d055c77fa549251d8b2a8a850918edbbf9941245ef6edbbb65d924edd7"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c4e7155fbdf89084abde1b33f05d681d8ffa0d5d07698d5d76a03ebdeb062848"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ed109a3eef13620c8ce57c429119990be08782c346465c265a23052e41e2cf42"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:777663011746b37b5df13df7826cb28ebc447b21ac8aa8278b7825404897dd5f"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66605ac59c9fbcd4159b0c0cfa239173ab77abc18cf714a1d0569cbabe3c836d"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-win32.whl", hash = "sha256:d909d0b217e85f366bfff45298966ea0dc49d76666fef2eb5777adc5b7aaa292"},
|
||||||
|
{file = "aiohttp-3.12.7-cp310-cp310-win_amd64.whl", hash = "sha256:362832e0b7c46c7ad3cf2f693061e17f1198f8d7fa4e907c304b3208241161c8"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:388b5947aa6931ef4ce3ed4edde6853e84980677886992cfadcf733dd06eed63"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ed5af1cce257cca27a3e920b003b3b397f63418a203064b7d804ea3b45782af"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f466ae8f9c02993b7d167be685bdbeb527cf254a3cfcc757697e0e336399d0a2"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2be095a420a9f9a12eff343d877ae180dd919238b539431af08cef929e874759"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b058cf2ba6adba699960d7bc403411c8a99ab5d3e5ea3eb01473638ae7d1a30e"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b6a660163b055686dbb0acc961978fd14537eba5d9da6cbdb4dced7a8d3be1a"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d741923905f267ad5d5c8f86a56f9d2beac9f32a36c217c5d9ef65cd74fd8ca0"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:519f5454b6018158ae0e789b8f6a88726c47dd680982eb318ef3ca4dee727314"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4eebfe470e22cc4b374d7e32c07e96d777a5c0fa51f3824de68e697da037ec"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:74ff39445f94923cf595e9e6dd602ecbe66b12364e2207e61342b8834417f8da"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:77cb9dba16486ecfeac8076763600b9714941e0ff696e53a30e8d408d9a196ca"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a7b3b9cbe83e3918a1918b0de274884f17b64224c1c9210a6fb0f7c10d246636"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6055f53c70938498884e71ca966abe8e9e7558489e13a7e40b6384dee7230d1d"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8493a42d5b2a736c6804239b985feebeea1c60f8fcb46a3607d6dce3c1a42b12"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:372f2237cade45f563d973c2a913895f2699a892c0eb11c55c6880b6f0acf219"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-win32.whl", hash = "sha256:41f686749a099b507563a5c0cb4fd77367b05448a2c1758784ad506a28e9e579"},
|
||||||
|
{file = "aiohttp-3.12.7-cp311-cp311-win_amd64.whl", hash = "sha256:7a3691583470d4397aca70fbf8e0f0778b63a2c2a6a23263bdeeb68395972f29"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9b9345918f5b5156a5712c37d1d331baf320df67547ea032a49a609b773c3606"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3091b4883f405dbabeb9ea821a25dec16d03a51c3e0d2752fc3ab48b652bf196"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:97fd97abd4cf199eff4041d0346a7dc68b60deab177f01de87283be513ffc3ab"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a5938973105cd5ff17176e8cb36bc19cac7c82ae7c58c0dbd7e023972d0c708"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e506ae5c4c05d1a1e87edd64b994cea2d49385d41d32e1c6be8764f31cf2245c"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b780b402e6361c4cfcec252580f5ecdd86cb68376520ac34748d3f8b262dd598"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf981bbfb7ff2ebc1b3bfae49d2efe2c51ca1cf3d90867f47c310df65398e85e"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94f98e0e5a49f89b252e115844f756c04fc8050f38252a32a3dd994ce8121f10"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:410e96cc6824fc4ced9703fb2ac2d06c6190d21fc6f5b588f62b1918628449c1"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:43e93987fe9df4349db8deae7c391695538c35e4ba893133c7e823234f6e4537"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb3f3dcb59f3e16819a1c7d3fa32e7b87255b661c1e139a1b5940bde270704ab"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4a46fe4a4c66b2712059e48a8384eb93565fbe3251af4844860fed846ef4ca75"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ad01793164661af70918490ef8efc2c09df7a3c686b6c84ca90a2d69cdbc3911"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e85c6833be3f49cead2e7bc79080e5c18d6dab9af32226ab5a01dc20c523e7d9"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c9f52149d8249566e72c50c7985c2345521b3b78f84aa86f6f492cd50b14793"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-win32.whl", hash = "sha256:0e1c33ac0f6a396bcefe9c1d52c9d38a051861885a5c102ca5c8298aba0108fa"},
|
||||||
|
{file = "aiohttp-3.12.7-cp312-cp312-win_amd64.whl", hash = "sha256:b4aed5233a9d13e34e8624ecb798533aa2da97e7048cc69671b7a6d7a2efe7e8"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:adbb2046600a60e37a54ea9b77b0ddef280029b0a853624a8e9b2b71a037c890"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76392cbadc1ccc0a8c02098b74c0240d53c644b10a81e1addbc1666dce3cd62a"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6f04af3bf040dc8fd9b2bc0e465f5aca6fc5349fa08bd7f08142974a2ded21bf"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b19763f88f058e9c605f79cde8a800660f7e259162b80982111cc631dfc54bf0"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6600550593c440ef29ca2a14b8a52ac91b9f494d85f75294409ec6ad5637476f"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c7b83c829be3cddaf958dee8108e09b1502c215e95064d3045015298dbded54a"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffa9928fd37061c8e35b85d3f1b4a256d0c3e8cbd421c1d8bd0ab45461b6a838"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc8086515dd1016b67db9ccebb7159234226dba99fb6a895a0c9270b644cf525"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c19b1de25703560fa64f998dfc3685040b52996056e048b3406c8e97dcfa1e3"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6357abdc7a2cfb113274c4f4a7f086bdca36905924953bf7a9e3f6add3aec7c5"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:29ff7876ff7e4a8029642334a81891cb5a842f1e405195c2946f697102756670"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5e7741c53d473204f89dd26f3b087a5883c742add8d6504d0d7d3ad3ff1cd1b7"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:38dc536059cc0624e22273905a1df74b231ac903d73af59ee6e6e3139f05a28b"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:82a59cf086396a409d6d2350c122aada07f1f56bb529734994d37bcafc8cf101"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7cd6e299292ba085a3642cb4085b393f45bbca45c067182d15e33c2e3473283c"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-win32.whl", hash = "sha256:4acec2b5de65adc469837260be8408d5f53d4c8ae60631be868e9d7eb8563167"},
|
||||||
|
{file = "aiohttp-3.12.7-cp313-cp313-win_amd64.whl", hash = "sha256:93317649d65cc895ba1fe5384353cb6c44638db39ebb55dabe3dade34a1b1177"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f85e48970aff5b00af94a5f6311ee0b61eca8bbc8769df39873fc68d747ca609"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bca9329faa73c42061a67b8b53e6b1d46b73e3411636bfe1d07c58d81067b902"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e80ef94a0993c7124b69bf1a95b5d26f22f24e5fdc6af22ca143105edde22ed"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e1f6e7825d3830ee85ddf5d322300d15053e94c66ff8b3d5e8ef0f152c0f1a"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:845a67d26ee9578d20738975591dccd0fcae7104c89cc112316787f9fdfe8b61"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1a280e27b2c772a9d69dfd0744929f8628a6b8b6e6e87c0125c8c417501a21"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:838a091be15ce619a83896c8485e814215f3383952dd58aec932d0f9ae85a02b"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ea0db720f2996f9b799c8ba6fbdd12063add509a81a398cd31a3fb152efae0d"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ccd1e07b61c26532f1a7908430c30d687425bbf2d4da26f09bc1f2acf06a5f9"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b1f532d312a42397e6f591499acf707cece6462f745c5670bb7c70d1bb963882"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:f8fa7c8ee01b54367cafb7e82947e36e57f9cb243d7c4d66e03fb96661b082ae"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:81a1ca045593149d3366286c30c57ebb63d2f28feca8ca3fae049c22ed8520c4"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9ca179427f7cbd3476eca3bfc229087c112b0418242c5b56f9f0f9c0e681b906"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4ee037aec7ccc8777b0f9603085a2c53108368443624f7dc834028b16591541"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2804245093897b77736540f75826d35587819e143f0f95e951bfea8eb312bf09"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-win32.whl", hash = "sha256:c8d9b576aa4e1359fcc479532b8a21803840fd61013eec875746b29c3930f073"},
|
||||||
|
{file = "aiohttp-3.12.7-cp39-cp39-win_amd64.whl", hash = "sha256:1496c9e785d0432a4eae6c059f1d68423fb6264cbdacaff2d9ab1859be66c5bb"},
|
||||||
|
{file = "aiohttp-3.12.7.tar.gz", hash = "sha256:08bf55b216c779eddb6e41c1841c17d7ddd12776c7d7b36051c0a292a9ca828e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
aiohappyeyeballs = ">=2.5.0"
|
||||||
|
aiosignal = ">=1.1.2"
|
||||||
|
attrs = ">=17.3.0"
|
||||||
|
frozenlist = ">=1.1.1"
|
||||||
|
multidict = ">=4.5,<7.0"
|
||||||
|
propcache = ">=0.2.0"
|
||||||
|
yarl = ">=1.17.0,<2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiosignal"
|
||||||
|
version = "1.3.2"
|
||||||
|
description = "aiosignal: a list of registered asynchronous callbacks"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"},
|
||||||
|
{file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
frozenlist = ">=1.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
|
|
@ -34,6 +181,26 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
|
||||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""]
|
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""]
|
||||||
trio = ["trio (>=0.26.1)"]
|
trio = ["trio (>=0.26.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "25.3.0"
|
||||||
|
description = "Classes Without Boilerplate"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"},
|
||||||
|
{file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
|
||||||
|
cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
|
||||||
|
dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
|
||||||
|
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"]
|
||||||
|
tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
|
||||||
|
tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bcrypt"
|
name = "bcrypt"
|
||||||
version = "4.3.0"
|
version = "4.3.0"
|
||||||
|
|
@ -395,6 +562,120 @@ uvicorn = {version = ">=0.15.0", extras = ["standard"]}
|
||||||
[package.extras]
|
[package.extras]
|
||||||
standard = ["uvicorn[standard] (>=0.15.0)"]
|
standard = ["uvicorn[standard] (>=0.15.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "frozenlist"
|
||||||
|
version = "1.6.0"
|
||||||
|
description = "A list-like structure which implements collections.abc.MutableSequence"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-win32.whl", hash = "sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812"},
|
||||||
|
{file = "frozenlist-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-win32.whl", hash = "sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860"},
|
||||||
|
{file = "frozenlist-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc"},
|
||||||
|
{file = "frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-win32.whl", hash = "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f"},
|
||||||
|
{file = "frozenlist-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:536a1236065c29980c15c7229fbb830dedf809708c10e159b8136534233545f0"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ed5e3a4462ff25ca84fb09e0fada8ea267df98a450340ead4c91b44857267d70"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e19c0fc9f4f030fcae43b4cdec9e8ab83ffe30ec10c79a4a43a04d1af6c5e1ad"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c608f833897501dac548585312d73a7dca028bf3b8688f0d712b7acfaf7fb3"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0dbae96c225d584f834b8d3cc688825911960f003a85cb0fd20b6e5512468c42"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:625170a91dd7261a1d1c2a0c1a353c9e55d21cd67d0852185a5fef86587e6f5f"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1db8b2fc7ee8a940b547a14c10e56560ad3ea6499dc6875c354e2335812f739d"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4da6fc43048b648275a220e3a61c33b7fff65d11bdd6dcb9d9c145ff708b804c"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef8e7e8f2f3820c5f175d70fdd199b79e417acf6c72c5d0aa8f63c9f721646f"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa733d123cc78245e9bb15f29b44ed9e5780dc6867cfc4e544717b91f980af3b"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ba7f8d97152b61f22d7f59491a781ba9b177dd9f318486c5fbc52cde2db12189"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:56a0b8dd6d0d3d971c91f1df75e824986667ccce91e20dca2023683814344791"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5c9e89bf19ca148efcc9e3c44fd4c09d5af85c8a7dd3dbd0da1cb83425ef4983"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1330f0a4376587face7637dfd245380a57fe21ae8f9d360c1c2ef8746c4195fa"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2187248203b59625566cac53572ec8c2647a140ee2738b4e36772930377a533c"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-win32.whl", hash = "sha256:2b8cf4cfea847d6c12af06091561a89740f1f67f331c3fa8623391905e878530"},
|
||||||
|
{file = "frozenlist-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:1255d5d64328c5a0d066ecb0f02034d086537925f1f04b50b1ae60d37afbf572"},
|
||||||
|
{file = "frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191"},
|
||||||
|
{file = "frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
|
@ -543,6 +824,37 @@ MarkupSafe = ">=2.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
i18n = ["Babel (>=2.7)"]
|
i18n = ["Babel (>=2.7)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac-vendor-lookup"
|
||||||
|
version = "0.1.12"
|
||||||
|
description = "Find the vendor for a given MAC address"
|
||||||
|
optional = false
|
||||||
|
python-versions = "<4, >=3.5"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "mac_vendor_lookup-0.1.12-py3-none-any.whl", hash = "sha256:aeec6eac01b07e6558d889b51f475a1e1e938e09cab409a069ab6a43b13cba58"},
|
||||||
|
{file = "mac_vendor_lookup-0.1.12.tar.gz", hash = "sha256:74e1723e177d4deb02977148d3fa04a7916f4bf93268e2afe3240529272bf80d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
aiofiles = "*"
|
||||||
|
aiohttp = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["coverage", "pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "manuf"
|
||||||
|
version = "1.1.5"
|
||||||
|
description = "Parser library for Wireshark's OUI database"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "manuf-1.1.5-py2-none-any.whl", hash = "sha256:0d6a34cbc7335e32af9dc75a39e37a8016e4593557e368a3ee2880ae1600819f"},
|
||||||
|
{file = "manuf-1.1.5.tar.gz", hash = "sha256:741989728c8ffd0d5c5782376c00270fcd48cf7544f58bf9492a0ef674d433cd"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markdown-it-py"
|
name = "markdown-it-py"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
|
|
@ -651,6 +963,120 @@ files = [
|
||||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multidict"
|
||||||
|
version = "6.4.4"
|
||||||
|
description = "multidict implementation"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8adee3ac041145ffe4488ea73fa0a622b464cc25340d98be76924d0cda8545ff"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b61e98c3e2a861035aaccd207da585bdcacef65fe01d7a0d07478efac005e028"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75493f28dbadecdbb59130e74fe935288813301a8554dc32f0c631b6bdcdf8b0"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc3c6a37e048b5395ee235e4a2a0d639c2349dffa32d9367a42fc20d399772"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87cb72263946b301570b0f63855569a24ee8758aaae2cd182aae7d95fbc92ca7"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bbf7bd39822fd07e3609b6b4467af4c404dd2b88ee314837ad1830a7f4a8299"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1f7cbd4f1f44ddf5fd86a8675b7679176eae770f2fc88115d6dddb6cefb59bc"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5ac9e5bfce0e6282e7f59ff7b7b9a74aa8e5c60d38186a4637f5aa764046ad"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4efc31dfef8c4eeb95b6b17d799eedad88c4902daba39ce637e23a17ea078915"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fcad2945b1b91c29ef2b4050f590bfcb68d8ac8e0995a74e659aa57e8d78e01"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d877447e7368c7320832acb7159557e49b21ea10ffeb135c1077dbbc0816b598"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:33a12ebac9f380714c298cbfd3e5b9c0c4e89c75fe612ae496512ee51028915f"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0f14ea68d29b43a9bf37953881b1e3eb75b2739e896ba4a6aa4ad4c5b9ffa145"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0327ad2c747a6600e4797d115d3c38a220fdb28e54983abe8964fd17e95ae83c"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d1a20707492db9719a05fc62ee215fd2c29b22b47c1b1ba347f9abc831e26683"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-win32.whl", hash = "sha256:d83f18315b9fca5db2452d1881ef20f79593c4aa824095b62cb280019ef7aa3d"},
|
||||||
|
{file = "multidict-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:9c17341ee04545fd962ae07330cb5a39977294c883485c8d74634669b1f7fe04"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-win32.whl", hash = "sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49"},
|
||||||
|
{file = "multidict-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b"},
|
||||||
|
{file = "multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e"},
|
||||||
|
{file = "multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:603f39bd1cf85705c6c1ba59644b480dfe495e6ee2b877908de93322705ad7cf"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc60f91c02e11dfbe3ff4e1219c085695c339af72d1641800fe6075b91850c8f"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:496bcf01c76a70a31c3d746fd39383aad8d685ce6331e4c709e9af4ced5fa221"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4219390fb5bf8e548e77b428bb36a21d9382960db5321b74d9d9987148074d6b"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef4e9096ff86dfdcbd4a78253090ba13b1d183daa11b973e842465d94ae1772"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49a29d7133b1fc214e818bbe025a77cc6025ed9a4f407d2850373ddde07fd04a"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e32053d6d3a8b0dfe49fde05b496731a0e6099a4df92154641c00aa76786aef5"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc403092a49509e8ef2d2fd636a8ecefc4698cc57bbe894606b14579bc2a955"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5363f9b2a7f3910e5c87d8b1855c478c05a2dc559ac57308117424dfaad6805c"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e543a40e4946cf70a88a3be87837a3ae0aebd9058ba49e91cacb0b2cd631e2b"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:60d849912350da557fe7de20aa8cf394aada6980d0052cc829eeda4a0db1c1db"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:19d08b4f22eae45bb018b9f06e2838c1e4b853c67628ef8ae126d99de0da6395"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d693307856d1ef08041e8b6ff01d5b4618715007d288490ce2c7e29013c12b9a"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fad6daaed41021934917f4fb03ca2db8d8a4d79bf89b17ebe77228eb6710c003"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c10d17371bff801af0daf8b073c30b6cf14215784dc08cd5c43ab5b7b8029bbc"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-win32.whl", hash = "sha256:7e23f2f841fcb3ebd4724a40032d32e0892fbba4143e43d2a9e7695c5e50e6bd"},
|
||||||
|
{file = "multidict-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:4d7b50b673ffb4ff4366e7ab43cf1f0aef4bd3608735c5fbdf0bdb6f690da411"},
|
||||||
|
{file = "multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac"},
|
||||||
|
{file = "multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "netaddr"
|
name = "netaddr"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
|
@ -749,6 +1175,114 @@ bcrypt = ["bcrypt (>=3.1.0)"]
|
||||||
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
|
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
|
||||||
totp = ["cryptography"]
|
totp = ["cryptography"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "propcache"
|
||||||
|
version = "0.3.1"
|
||||||
|
description = "Accelerated property cache"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"},
|
||||||
|
{file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"},
|
||||||
|
{file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"},
|
||||||
|
{file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"},
|
||||||
|
{file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64"},
|
||||||
|
{file = "propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566"},
|
||||||
|
{file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"},
|
||||||
|
{file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyasn1"
|
name = "pyasn1"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
|
|
@ -1059,6 +1593,18 @@ files = [
|
||||||
{file = "python_pam-2.0.2-py3-none-any.whl", hash = "sha256:4ac51dd8953ac59aa45505882b565eef6a22e0423dcf25d63369902080416c20"},
|
{file = "python_pam-2.0.2-py3-none-any.whl", hash = "sha256:4ac51dd8953ac59aa45505882b565eef6a22e0423dcf25d63369902080416c20"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pywifi"
|
||||||
|
version = "1.1.12"
|
||||||
|
description = "A cross-platform module for manipulating WiFi devices."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "pywifi-1.1.12-py3-none-any.whl", hash = "sha256:d1b79d9ff054eef1aca4ab97f2641650d001d6740bfa7666a7d9506bcfb8459b"},
|
||||||
|
{file = "pywifi-1.1.12.tar.gz", hash = "sha256:6da15de5d573635a461a95919920788b6f61748e1bd93a5e3504f456cf2e8bcc"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.2"
|
version = "6.0.2"
|
||||||
|
|
@ -1569,7 +2115,126 @@ files = [
|
||||||
{file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"},
|
{file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yarl"
|
||||||
|
version = "1.20.0"
|
||||||
|
description = "Yet another URL library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d"},
|
||||||
|
{file = "yarl-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6"},
|
||||||
|
{file = "yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64"},
|
||||||
|
{file = "yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac"},
|
||||||
|
{file = "yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-win32.whl", hash = "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8"},
|
||||||
|
{file = "yarl-1.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7"},
|
||||||
|
{file = "yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124"},
|
||||||
|
{file = "yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
idna = ">=2.0"
|
||||||
|
multidict = ">=4.0"
|
||||||
|
propcache = ">=0.2.1"
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "a9465e2172fc3fe6ee66a3df67343b3e245e723d4244d8e85534df3ed1696369"
|
content-hash = "6dfae73c0cdec175259066cdcf6fe6bceefc5ddfc84252b6093d1abf3ec82d58"
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ packages = [
|
||||||
{include = "models"},
|
{include = "models"},
|
||||||
{include = "services"},
|
{include = "services"},
|
||||||
{include = "core"},
|
{include = "core"},
|
||||||
{include = "tests"},
|
|
||||||
{include = "storage"},
|
{include = "storage"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -36,6 +35,9 @@ storage = "^0.0.4.3"
|
||||||
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
|
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
|
||||||
python-multipart = "^0.0.20"
|
python-multipart = "^0.0.20"
|
||||||
bcrypt = "^4.3.0"
|
bcrypt = "^4.3.0"
|
||||||
|
mac-vendor-lookup = "^0.1.12"
|
||||||
|
pywifi = "^1.1.12"
|
||||||
|
manuf = "^1.1.5"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,27 @@ from scapy.sendrecv import srp
|
||||||
import netifaces
|
import netifaces
|
||||||
from netaddr import IPNetwork
|
from netaddr import IPNetwork
|
||||||
|
|
||||||
|
|
||||||
class NetworkScanner:
|
class NetworkScanner:
|
||||||
"""Иницилазицая сетевого сканера"""
|
"""Иницилазицая сетевого сканера"""
|
||||||
|
|
||||||
def __init__(self, interface=None):
|
def __init__(self, interface=None):
|
||||||
"""Инициализация параметров интерфейсов """
|
"""Инициализация параметров интерфейсов"""
|
||||||
self.interface = interface or netifaces.gateways()['default'][netifaces.AF_INET][1]
|
self.interface = (
|
||||||
self.ip_info = netifaces.ifaddresses(self.interface).get(netifaces.AF_INET, [])
|
interface or netifaces.gateways()["default"][netifaces.AF_INET][1])
|
||||||
self.networks = [IPNetwork(f"{ip['addr']}/{ip['netmask']}") for ip in self.ip_info]
|
self.ip_info = netifaces.ifaddresses(self.interface).get(
|
||||||
self.local_mac = netifaces.ifaddresses(self.interface)[netifaces.AF_LINK][0]['addr']
|
netifaces.AF_INET, [])
|
||||||
|
self.networks = [
|
||||||
|
IPNetwork(f"{ip['addr']}/{ip['netmask']}") for ip in self.ip_info
|
||||||
|
]
|
||||||
|
self.local_mac = netifaces.ifaddresses(
|
||||||
|
self.interface)[netifaces.AF_LINK][0]["addr"]
|
||||||
|
|
||||||
def _create_arp_packet(self, target_ip):
|
def _create_arp_packet(self, target_ip):
|
||||||
"""Создание пакета ARP для отправки"""
|
"""Создание пакета ARP для отправки"""
|
||||||
arp = ARP(pdst=target_ip)
|
arp = ARP(pdst=target_ip)
|
||||||
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
|
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
|
||||||
return ether/arp
|
return ether / arp
|
||||||
|
|
||||||
def scan_network(self, attempts=3, timeout=2):
|
def scan_network(self, attempts=3, timeout=2):
|
||||||
"""Подготовка к сканированию сети"""
|
"""Подготовка к сканированию сети"""
|
||||||
|
|
@ -28,9 +35,12 @@ class NetworkScanner:
|
||||||
clients = []
|
clients = []
|
||||||
|
|
||||||
for _ in range(attempts):
|
for _ in range(attempts):
|
||||||
result = srp(packet, timeout=timeout, iface=self.interface, verbose=0)[0]
|
result = srp(packet,
|
||||||
|
timeout=timeout,
|
||||||
|
iface=self.interface,
|
||||||
|
verbose=0)[0]
|
||||||
for _, received in result:
|
for _, received in result:
|
||||||
client = {'ip': received.psrc, 'mac': received.hwsrc}
|
client = {"ip": received.psrc, "mac": received.hwsrc}
|
||||||
if client not in clients: # Избегаем дублирования клиентов
|
if client not in clients: # Избегаем дублирования клиентов
|
||||||
clients.append(client)
|
clients.append(client)
|
||||||
|
|
||||||
|
|
@ -38,7 +48,7 @@ class NetworkScanner:
|
||||||
|
|
||||||
# Добавляет все локальные IP-адреса интерфейса как клиенты
|
# Добавляет все локальные IP-адреса интерфейса как клиенты
|
||||||
for ip_info in self.ip_info:
|
for ip_info in self.ip_info:
|
||||||
local_client = {'ip': ip_info['addr'], 'mac': self.local_mac}
|
local_client = {"ip": ip_info["addr"], "mac": self.local_mac}
|
||||||
if local_client not in all_clients:
|
if local_client not in all_clients:
|
||||||
all_clients.append(local_client)
|
all_clients.append(local_client)
|
||||||
|
|
||||||
|
|
@ -49,10 +59,11 @@ class NetworkScanner:
|
||||||
def print_clients(self, clients):
|
def print_clients(self, clients):
|
||||||
"""Лист адресов клиентов"""
|
"""Лист адресов клиентов"""
|
||||||
print("Доступные устройства в сети:")
|
print("Доступные устройства в сети:")
|
||||||
print("IP" + " "*18+"MAC")
|
print("IP" + " " * 18 + "MAC")
|
||||||
for client in sorted(clients, key=lambda x: x['ip']):
|
for client in sorted(clients, key=lambda x: x["ip"]):
|
||||||
print(f"{client['ip']:<16} {client['mac']}")
|
print(f"{client['ip']:<16} {client['mac']}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
scanner = NetworkScanner()
|
scanner = NetworkScanner()
|
||||||
clients = scanner.scan_network()
|
clients = scanner.scan_network()
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,10 @@ ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
||||||
|
|
||||||
|
|
||||||
class AuthService:
|
class AuthService:
|
||||||
"""Сервис для регистрации, авторизации и управления сессиями"""
|
"""Сервис для регистрации, авторизации и управления сессиями"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.storage = JsonStorage()
|
self.storage = JsonStorage()
|
||||||
self.session_storage = SessionStorage()
|
self.session_storage = SessionStorage()
|
||||||
|
|
@ -35,7 +37,8 @@ class AuthService:
|
||||||
def create_access_token(self, data: dict) -> str:
|
def create_access_token(self, data: dict) -> str:
|
||||||
"""Создаёт токен"""
|
"""Создаёт токен"""
|
||||||
to_encode = data.copy()
|
to_encode = data.copy()
|
||||||
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
expire = datetime.utcnow() + timedelta(
|
||||||
|
minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
to_encode.update({"exp": expire})
|
to_encode.update({"exp": expire})
|
||||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
return encoded_jwt
|
return encoded_jwt
|
||||||
|
|
@ -57,10 +60,14 @@ class AuthService:
|
||||||
db_user = self.storage.create_user(user)
|
db_user = self.storage.create_user(user)
|
||||||
return db_user
|
return db_user
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"Ошибка при регистрации пользователя {user.username}: {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при регистрации пользователя {user.username}: {str(e)}"
|
||||||
|
)
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при регистрации пользователя {user.username}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при регистрации пользователя {user.username}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise HTTPException(status_code=500, detail="Ошибка регистрации")
|
raise HTTPException(status_code=500, detail="Ошибка регистрации")
|
||||||
|
|
||||||
def login(self, user: UserLogin, client_id: str) -> dict:
|
def login(self, user: UserLogin, client_id: str) -> dict:
|
||||||
|
|
@ -69,24 +76,41 @@ class AuthService:
|
||||||
db_user = self.storage.get_user_by_username(user.username)
|
db_user = self.storage.get_user_by_username(user.username)
|
||||||
if not db_user:
|
if not db_user:
|
||||||
logger.error(f"Пользователь {user.username} не найден")
|
logger.error(f"Пользователь {user.username} не найден")
|
||||||
raise HTTPException(status_code=401, detail="Неверный логин или пароль")
|
raise HTTPException(status_code=401,
|
||||||
if not self.storage.verify_password(user.password, db_user["password_hash"]):
|
detail="Неверный логин или пароль")
|
||||||
logger.error(f"Неверный пароль для пользователя {user.username}")
|
if not self.storage.verify_password(user.password,
|
||||||
raise HTTPException(status_code=401, detail="Неверный логин или пароль")
|
db_user["password_hash"]):
|
||||||
access_token = self.create_access_token(data={"sub": user.username})
|
logger.error(
|
||||||
session_id = self.session_storage.create_session(user.username, access_token, client_id)
|
f"Неверный пароль для пользователя {user.username}")
|
||||||
|
raise HTTPException(status_code=401,
|
||||||
|
detail="Неверный логин или пароль")
|
||||||
|
access_token = self.create_access_token(
|
||||||
|
data={"sub": user.username})
|
||||||
|
session_id = self.session_storage.create_session(
|
||||||
|
user.username, access_token, client_id)
|
||||||
logger.info(f"Пользователь {user.username} успешно авторизован")
|
logger.info(f"Пользователь {user.username} успешно авторизован")
|
||||||
return {
|
return {
|
||||||
"access_token": access_token,
|
"access_token":
|
||||||
"token_type": "bearer",
|
access_token,
|
||||||
"session_id": session_id,
|
"token_type":
|
||||||
"user": UserResponse(id=db_user["id"], username=db_user["username"], is_admin=db_user["is_admin"])
|
"bearer",
|
||||||
|
"session_id":
|
||||||
|
session_id,
|
||||||
|
"user":
|
||||||
|
UserResponse(
|
||||||
|
id=db_user["id"],
|
||||||
|
username=db_user["username"],
|
||||||
|
is_admin=db_user["is_admin"],
|
||||||
|
),
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при авторизации пользователя {user.username}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при авторизации пользователя {user.username}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise HTTPException(status_code=500, detail="Ошибка авторизации")
|
raise HTTPException(status_code=500, detail="Ошибка авторизации")
|
||||||
|
|
||||||
def get_current_user(self, token: str = Depends(oauth2_scheme)) -> UserResponse:
|
def get_current_user(
|
||||||
|
self, token: str = Depends(oauth2_scheme)) -> UserResponse:
|
||||||
"""Получает текущего пользователя по токену"""
|
"""Получает текущего пользователя по токену"""
|
||||||
credentials = self.verify_token(token)
|
credentials = self.verify_token(token)
|
||||||
if not credentials:
|
if not credentials:
|
||||||
|
|
@ -96,24 +120,36 @@ class AuthService:
|
||||||
db_user = self.storage.get_user_by_username(username)
|
db_user = self.storage.get_user_by_username(username)
|
||||||
if not db_user:
|
if not db_user:
|
||||||
logger.error(f"Пользователь {username} не найден")
|
logger.error(f"Пользователь {username} не найден")
|
||||||
raise HTTPException(status_code=401, detail="Пользователь не найден")
|
raise HTTPException(status_code=401,
|
||||||
return UserResponse(id=db_user["id"], username=db_user["username"], is_admin=db_user["is_admin"])
|
detail="Пользователь не найден")
|
||||||
|
return UserResponse(id=db_user["id"],
|
||||||
|
username=db_user["username"],
|
||||||
|
is_admin=db_user["is_admin"])
|
||||||
|
|
||||||
def get_user_by_session(self, client_id: str) -> dict:
|
def get_user_by_session(self, client_id: str) -> dict:
|
||||||
"""Получает пользователя и токен по client_id"""
|
"""Получает пользователя и токен по client_id"""
|
||||||
session = self.session_storage.get_latest_session(client_id)
|
session = self.session_storage.get_latest_session(client_id)
|
||||||
if not session:
|
if not session:
|
||||||
logger.error(f"Сессия для client_id {client_id} не найдена или истекла")
|
logger.error(
|
||||||
raise HTTPException(status_code=401, detail="Сессия недействительна")
|
f"Сессия для client_id {client_id} не найдена или истекла")
|
||||||
|
raise HTTPException(status_code=401,
|
||||||
|
detail="Сессия недействительна")
|
||||||
username = session["username"]
|
username = session["username"]
|
||||||
token = session["token"]
|
token = session["token"]
|
||||||
db_user = self.storage.get_user_by_username(username)
|
db_user = self.storage.get_user_by_username(username)
|
||||||
if not db_user:
|
if not db_user:
|
||||||
logger.error(f"Пользователь {username} не найден")
|
logger.error(f"Пользователь {username} не найден")
|
||||||
raise HTTPException(status_code=401, detail="Пользователь не найден")
|
raise HTTPException(status_code=401,
|
||||||
|
detail="Пользователь не найден")
|
||||||
return {
|
return {
|
||||||
"user": UserResponse(id=db_user["id"], username=db_user["username"], is_admin=db_user["is_admin"]),
|
"user":
|
||||||
"access_token": token
|
UserResponse(
|
||||||
|
id=db_user["id"],
|
||||||
|
username=db_user["username"],
|
||||||
|
is_admin=db_user["is_admin"],
|
||||||
|
),
|
||||||
|
"access_token":
|
||||||
|
token,
|
||||||
}
|
}
|
||||||
|
|
||||||
def logout(self, client_id: str):
|
def logout(self, client_id: str):
|
||||||
|
|
@ -122,8 +158,12 @@ class AuthService:
|
||||||
session = self.session_storage.get_latest_session(client_id)
|
session = self.session_storage.get_latest_session(client_id)
|
||||||
if session:
|
if session:
|
||||||
self.session_storage.delete_session(session["session_id"])
|
self.session_storage.delete_session(session["session_id"])
|
||||||
logger.info(f"Пользователь успешно вышел из системы для client_id {client_id}")
|
logger.info(
|
||||||
|
f"Пользователь успешно вышел из системы для client_id {client_id}"
|
||||||
|
)
|
||||||
return {"message": "Успешный выход из системы"}
|
return {"message": "Успешный выход из системы"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при выходе из системы: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
raise HTTPException(status_code=500, detail="Ошибка при выходе из системы")
|
f"Ошибка при выходе из системы: {type(e).__name__} - {str(e)}")
|
||||||
|
raise HTTPException(status_code=500,
|
||||||
|
detail="Ошибка при выходе из системы")
|
||||||
|
|
|
||||||
106
SRV/services/connect_service.py
Normal file
106
SRV/services/connect_service.py
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class WifiConnectionService:
|
||||||
|
def __init__(self):
|
||||||
|
self.interface = self._get_wifi_interface()
|
||||||
|
|
||||||
|
def _get_wifi_interface(self) -> str:
|
||||||
|
"""Автоматически определяет Wi-Fi-интерфейс."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["nmcli", "radio", "wifi"], capture_output=True, text=True)
|
||||||
|
if "enabled" in result.stdout.lower():
|
||||||
|
interfaces = subprocess.run(["nmcli", "device", "status"], capture_output=True, text=True).stdout
|
||||||
|
for line in interfaces.splitlines():
|
||||||
|
if "wifi" in line.lower() and "connected" in line.lower():
|
||||||
|
iface = line.split()[0]
|
||||||
|
logger.info(f"Используется Wi-Fi-интерфейс: {iface}")
|
||||||
|
return iface
|
||||||
|
logger.warning("Wi-Fi-интерфейс не найден или не подключён")
|
||||||
|
return "wlan0"
|
||||||
|
logger.error("Wi-Fi радио отключено")
|
||||||
|
return ""
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при определении Wi-Fi-интерфейса: {str(e)}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def connect(self, ssid: str, password: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Подключается к Wi-Fi сети по SSID и паролю."""
|
||||||
|
try:
|
||||||
|
if not self.interface:
|
||||||
|
return False, "Wi-Fi-интерфейс не доступен"
|
||||||
|
|
||||||
|
result = subprocess.run(["nmcli", "-f", "SSID", "device", "wifi", "list"], capture_output=True, text=True)
|
||||||
|
if ssid not in result.stdout:
|
||||||
|
return False, f"Сеть с SSID {ssid} не найдена в списке доступных сетей"
|
||||||
|
|
||||||
|
cmd = ["nmcli", "device", "wifi", "connect", ssid, "password", password]
|
||||||
|
process = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||||
|
if process.returncode != 0:
|
||||||
|
error_msg = process.stderr.lower()
|
||||||
|
if "secrets were required" in error_msg or "authentication failed" in error_msg or "802-11-wireless-security.psk: недействительное свойство" in process.stderr:
|
||||||
|
return False, f"Ошибка подключения к {ssid}: Неверный пароль"
|
||||||
|
elif "no network with ssid" in error_msg:
|
||||||
|
return False, f"Ошибка подключения к {ssid}: Сеть с SSID {ssid} недоступна или неверно указана"
|
||||||
|
else:
|
||||||
|
return False, f"Ошибка подключения к {ssid}: {process.stderr}"
|
||||||
|
logger.info(f"Успешное подключение к сети {ssid}")
|
||||||
|
return True, None
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"Ошибка подключения к {ssid}: {e.stderr}")
|
||||||
|
error_msg = e.stderr.lower()
|
||||||
|
if "secrets were required" in error_msg or "authentication failed" in error_msg or "802-11-wireless-security.psk: недействительное свойство" in e.stderr:
|
||||||
|
return False, f"Ошибка подключения к {ssid}: Неверный пароль"
|
||||||
|
elif "no network with ssid" in error_msg:
|
||||||
|
return False, f"Ошибка подключения к {ssid}: Сеть с SSID {ssid} недоступна или неверно указана"
|
||||||
|
return False, f"Ошибка подключения к {ssid}: {e.stderr}"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Неизвестная ошибка при подключении к {ssid}: {str(e)}")
|
||||||
|
return False, f"Ошибка подключения к {ssid}: Неизвестная ошибка - {str(e)}"
|
||||||
|
|
||||||
|
def disconnect(self, ssid: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Отключается от Wi-Fi сети по указанному SSID."""
|
||||||
|
try:
|
||||||
|
if not self.interface:
|
||||||
|
return False, "Wi-Fi-интерфейс не доступен"
|
||||||
|
result = subprocess.run(["nmcli", "connection", "show", "--active"], capture_output=True, text=True)
|
||||||
|
active_connections = result.stdout.strip().splitlines()
|
||||||
|
if not active_connections:
|
||||||
|
logger.info(f"Нет активных подключений для SSID {ssid}")
|
||||||
|
return True, f"Нет активного подключения для SSID {ssid}"
|
||||||
|
for conn in active_connections:
|
||||||
|
conn_details = conn.split()
|
||||||
|
if len(conn_details) > 1:
|
||||||
|
conn_name = conn_details[0]
|
||||||
|
conn_info = subprocess.run(["nmcli", "connection", "show", conn_name], capture_output=True, text=True).stdout
|
||||||
|
if "802-11-wireless.ssid:" in conn_info and ssid in conn_info:
|
||||||
|
subprocess.run(["nmcli", "connection", "down", conn_name], capture_output=True, text=True, check=True)
|
||||||
|
logger.info(f"Отключено соединение с SSID {ssid}")
|
||||||
|
return True, None
|
||||||
|
logger.warning(f"Активное подключение с SSID {ssid} не найдено")
|
||||||
|
return True, f"Активное подключение с SSID {ssid} не найдено"
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"Ошибка при отключении от {ssid}: {e.stderr}")
|
||||||
|
return False, f"Ошибка отключения от {ssid}: {e.stderr}"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Неизвестная ошибка при отключении от {ssid}: {str(e)}")
|
||||||
|
return False, f"Ошибка отключения от {ssid}: {str(e)}"
|
||||||
|
|
||||||
|
def get_status(self) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Проверяет статус текущего подключения к Wi-Fi."""
|
||||||
|
try:
|
||||||
|
if not self.interface:
|
||||||
|
return False, "Wi-Fi-интерфейс не доступен"
|
||||||
|
result = subprocess.run(["nmcli", "connection", "show", "--active"], capture_output=True, text=True)
|
||||||
|
if result.stdout.strip():
|
||||||
|
ssid = result.stdout.splitlines()[1].split()[0]
|
||||||
|
logger.info(f"Текущая сеть: {ssid}")
|
||||||
|
return True, ssid
|
||||||
|
return False, "Не подключено к Wi-Fi"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при проверке статуса: {str(e)}")
|
||||||
|
return False, f"Ошибка проверки статуса: {str(e)}"
|
||||||
|
|
@ -7,11 +7,11 @@ DEBUG = True
|
||||||
|
|
||||||
|
|
||||||
class FirewalldService:
|
class FirewalldService:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.bus = SystemBus()
|
self.bus = SystemBus()
|
||||||
self.proxy = self.bus.get(
|
self.proxy = self.bus.get("org.fedoraproject.FirewallD1",
|
||||||
"org.fedoraproject.FirewallD1", "/org/fedoraproject/FirewallD1"
|
"/org/fedoraproject/FirewallD1")
|
||||||
)
|
|
||||||
|
|
||||||
def get_zones(self) -> List[str]:
|
def get_zones(self) -> List[str]:
|
||||||
"""Get list of all zones."""
|
"""Get list of all zones."""
|
||||||
|
|
@ -34,12 +34,16 @@ class FirewalldService:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Failed to set default zone to {zone}: {str(e)}")
|
raise Exception(f"Failed to set default zone to {zone}: {str(e)}")
|
||||||
|
|
||||||
def add_service(self, zone: str, service: str, timeout: Optional[int] = 0) -> None:
|
def add_service(self,
|
||||||
|
zone: str,
|
||||||
|
service: str,
|
||||||
|
timeout: Optional[int] = 0) -> None:
|
||||||
"""Add a service to a zone."""
|
"""Add a service to a zone."""
|
||||||
try:
|
try:
|
||||||
self.proxy.addService(zone, service, timeout)
|
self.proxy.addService(zone, service, timeout)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Failed to add service {service} to zone {zone}: {str(e)}")
|
raise Exception(
|
||||||
|
f"Failed to add service {service} to zone {zone}: {str(e)}")
|
||||||
|
|
||||||
def remove_service(self, zone: str, service: str) -> None:
|
def remove_service(self, zone: str, service: str) -> None:
|
||||||
"""Remove a service from a zone."""
|
"""Remove a service from a zone."""
|
||||||
|
|
@ -55,22 +59,23 @@ class FirewalldService:
|
||||||
try:
|
try:
|
||||||
return self.proxy.getServices(zone)
|
return self.proxy.getServices(zone)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Failed to get services for zone {zone}: {str(e)}")
|
raise Exception(
|
||||||
|
f"Failed to get services for zone {zone}: {str(e)}")
|
||||||
|
|
||||||
def add_port(
|
def add_port(self,
|
||||||
self, zone: str, port: int, protocol: str, timeout: Optional[int] = 0
|
zone: str,
|
||||||
) -> None:
|
port: int,
|
||||||
|
protocol: str,
|
||||||
|
timeout: Optional[int] = 0) -> None:
|
||||||
"""Add a port to a zone."""
|
"""Add a port to a zone."""
|
||||||
try:
|
try:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
print(f"Adding port {port}/{protocol} to zone {zone}")
|
print(f"Adding port {port}/{protocol} to zone {zone}")
|
||||||
print(f"Timeout: {timeout}")
|
print(f"Timeout: {timeout}")
|
||||||
# Get the zone interface
|
# Get the zone interface
|
||||||
zone_proxy = self.bus.get(
|
zone_proxy = self.bus.get("org.fedoraproject.FirewallD1",
|
||||||
"org.fedoraproject.FirewallD1",
|
"/org/fedoraproject/FirewallD1")
|
||||||
"/org/fedoraproject/FirewallD1"
|
zone_iface = zone_proxy["org.fedoraproject.FirewallD1.zone"]
|
||||||
)
|
|
||||||
zone_iface = zone_proxy['org.fedoraproject.FirewallD1.zone']
|
|
||||||
# Convert port to string
|
# Convert port to string
|
||||||
port_str = str(port)
|
port_str = str(port)
|
||||||
# Convert timeout to integer
|
# Convert timeout to integer
|
||||||
|
|
@ -106,7 +111,8 @@ class FirewalldService:
|
||||||
try:
|
try:
|
||||||
self.proxy.addSource(zone, source)
|
self.proxy.addSource(zone, source)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Failed to add source {source} to zone {zone}: {str(e)}")
|
raise Exception(
|
||||||
|
f"Failed to add source {source} to zone {zone}: {str(e)}")
|
||||||
|
|
||||||
def remove_source(self, zone: str, source: str) -> None:
|
def remove_source(self, zone: str, source: str) -> None:
|
||||||
"""Remove a source from a zone."""
|
"""Remove a source from a zone."""
|
||||||
|
|
@ -114,8 +120,7 @@ class FirewalldService:
|
||||||
self.proxy.removeSource(zone, source)
|
self.proxy.removeSource(zone, source)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"Failed to remove source {source} from zone {zone}: {str(e)}"
|
f"Failed to remove source {source} from zone {zone}: {str(e)}")
|
||||||
)
|
|
||||||
|
|
||||||
def get_sources(self, zone: str) -> List[str]:
|
def get_sources(self, zone: str) -> List[str]:
|
||||||
"""Get list of sources in a zone."""
|
"""Get list of sources in a zone."""
|
||||||
|
|
@ -140,4 +145,5 @@ class FirewalldService:
|
||||||
"sources": self.get_sources(zone),
|
"sources": self.get_sources(zone),
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Failed to get zone settings for {zone}: {str(e)}")
|
raise Exception(
|
||||||
|
f"Failed to get zone settings for {zone}: {str(e)}")
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,66 @@
|
||||||
from sdbus_async.networkmanager import NetworkManager, NetworkManagerSettings, NetworkConnectionSettings, NetworkDeviceGeneric, ActiveConnection
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from sdbus_async.networkmanager.settings import ConnectionProfile
|
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
from sdbus_async.networkmanager import (
|
||||||
|
NetworkManager,
|
||||||
|
NetworkManagerSettings,
|
||||||
|
NetworkConnectionSettings,
|
||||||
|
NetworkDeviceGeneric,
|
||||||
|
)
|
||||||
import sdbus
|
import sdbus
|
||||||
|
from sdbus_async.networkmanager.settings import ConnectionProfile
|
||||||
from services.arp_service import NetworkScanner
|
from services.arp_service import NetworkScanner
|
||||||
|
|
||||||
|
|
||||||
class IPService:
|
class IPService:
|
||||||
"""
|
"""
|
||||||
Сервис проверки доступности IP-адреса и применения настроек сети
|
Сервис проверки доступности IP-адреса и применения настроек сети
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.arp_scanner = NetworkScanner()
|
self.arp_scanner = NetworkScanner()
|
||||||
self.bus = sdbus.sd_bus_open_system()
|
self.bus = sdbus.sd_bus_open_system()
|
||||||
sdbus.set_default_bus(self.bus)
|
sdbus.set_default_bus(self.bus)
|
||||||
|
|
||||||
def _reset_bus(self):
|
def _reset_bus(self):
|
||||||
""" Перезапускает соединение с системной шиной D-Bus """
|
"""Перезапускает соединение с системной шиной D-Bus"""
|
||||||
new_bus = sdbus.sd_bus_open_system()
|
new_bus = sdbus.sd_bus_open_system()
|
||||||
sdbus.set_default_bus(new_bus)
|
sdbus.set_default_bus(new_bus)
|
||||||
if hasattr(self, 'bus') and self.bus is not None:
|
if hasattr(self, "bus") and self.bus is not None:
|
||||||
self.bus = None
|
self.bus = None
|
||||||
self.bus = new_bus
|
self.bus = new_bus
|
||||||
|
|
||||||
async def check_ip_config(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
async def check_ip_config(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
""" Проверяет доступность IP-адреса перед его применением """
|
"""Проверяет доступность IP-адреса перед его применением"""
|
||||||
self._reset_bus()
|
self._reset_bus()
|
||||||
clients = self.arp_scanner.scan_network()
|
clients = self.arp_scanner.scan_network()
|
||||||
new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"]
|
new_ip = args["ipv4_updates"]["ipv4.address-data"][0]["address"]
|
||||||
if any(client['ip'] == new_ip for client in clients):
|
if any(client["ip"] == new_ip for client in clients):
|
||||||
return {"available": False, "message": f"IP address {new_ip} уже занят."}
|
return {
|
||||||
return {"available": True, "message": "Network profile settings are available for use."}
|
"available": False,
|
||||||
|
"message": f"IP address {new_ip} уже занят."
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"available": True,
|
||||||
|
"message": "Network profile settings are available for use.",
|
||||||
|
}
|
||||||
|
|
||||||
async def apply_ip_config(self, args: Dict[str, Any]) -> None:
|
async def apply_ip_config(self, args: Dict[str, Any]) -> None:
|
||||||
""" Применяет проверенные настройки IP """
|
"""Применяет проверенные настройки IP"""
|
||||||
self._reset_bus()
|
self._reset_bus()
|
||||||
nm = NetworkManager(self.bus)
|
nm = NetworkManager()
|
||||||
settings = NetworkManagerSettings(self.bus)
|
settings = NetworkManagerSettings()
|
||||||
connection_paths = await settings.get_connections_by_id(args["connection_id"])
|
connection_paths = await settings.get_connections_by_id(
|
||||||
|
args["connection_id"])
|
||||||
|
|
||||||
if not connection_paths:
|
if not connection_paths:
|
||||||
raise ValueError(f"Подключение {args['connection_id']} не найдено")
|
raise ValueError(f"Подключение {args['connection_id']} не найдено")
|
||||||
|
|
||||||
connection = NetworkConnectionSettings(connection_paths[0], self.bus)
|
connection = NetworkConnectionSettings(connection_paths[0])
|
||||||
profile = await connection.get_profile()
|
profile = await connection.get_profile()
|
||||||
settings_dict = profile.to_settings_dict()
|
settings_dict = profile.to_settings_dict()
|
||||||
|
|
||||||
# Обновляем настройки IPv4
|
|
||||||
for key_path, value in args["ipv4_updates"].items():
|
for key_path, value in args["ipv4_updates"].items():
|
||||||
section, *props = key_path.split('.')
|
section, *props = key_path.split(".")
|
||||||
current = settings_dict[section]
|
current = settings_dict[section]
|
||||||
for prop in props[:-1]:
|
for prop in props[:-1]:
|
||||||
current = current.setdefault(prop, {})
|
current = current.setdefault(prop, {})
|
||||||
|
|
@ -57,56 +70,17 @@ class IPService:
|
||||||
await connection.update(new_profile.to_dbus())
|
await connection.update(new_profile.to_dbus())
|
||||||
print(f"Обновлены настройки для {args['connection_id']}:")
|
print(f"Обновлены настройки для {args['connection_id']}:")
|
||||||
|
|
||||||
# Проверяем interface-name
|
interface_name = settings_dict["connection"]["interface-name"]
|
||||||
interface_name = settings_dict['connection'].get('interface-name')
|
|
||||||
if not interface_name:
|
|
||||||
# Ищем устройство, связанное с connection_id через активное соединение
|
|
||||||
devices = await nm.get_devices()
|
devices = await nm.get_devices()
|
||||||
for device_path in devices:
|
for device_path in devices:
|
||||||
device = NetworkDeviceGeneric(device_path, self.bus)
|
device = NetworkDeviceGeneric(device_path)
|
||||||
active_connection = await device.active_connection
|
|
||||||
if active_connection != '/':
|
|
||||||
active_conn = ActiveConnection(active_connection, self.bus)
|
|
||||||
conn_path = await active_conn.connection # Путь к настройкам соединения
|
|
||||||
if conn_path != '/':
|
|
||||||
conn_settings = NetworkConnectionSettings(conn_path, self.bus)
|
|
||||||
active_profile = await conn_settings.get_profile()
|
|
||||||
if active_profile.to_settings_dict()['connection']['id'] == args["connection_id"]:
|
|
||||||
interface_name = await device.interface
|
|
||||||
settings_dict['connection']['interface-name'] = interface_name
|
|
||||||
new_profile = ConnectionProfile.from_settings_dict(settings_dict)
|
|
||||||
await connection.update(new_profile.to_dbus())
|
|
||||||
print(f"Добавлен interface-name: {interface_name} для {args['connection_id']}")
|
|
||||||
break
|
|
||||||
|
|
||||||
if not interface_name:
|
|
||||||
# Если не нашли активное соединение, пробуем получить интерфейс из настроек устройства
|
|
||||||
devices = await nm.get_devices()
|
|
||||||
for device_path in devices:
|
|
||||||
device = NetworkDeviceGeneric(device_path, self.bus)
|
|
||||||
applied_connection = await device.get_applied_connection()
|
|
||||||
connection_settings = applied_connection[0]
|
|
||||||
conn_id = connection_settings.get('connection', {}).get('id', ('s', ''))[1]
|
|
||||||
if conn_id == args["connection_id"]:
|
|
||||||
interface_name = await device.interface
|
|
||||||
settings_dict['connection']['interface-name'] = interface_name
|
|
||||||
new_profile = ConnectionProfile.from_settings_dict(settings_dict)
|
|
||||||
await connection.update(new_profile.to_dbus())
|
|
||||||
print(f"Добавлен interface-name: {interface_name} для {args['connection_id']}")
|
|
||||||
break
|
|
||||||
|
|
||||||
if not interface_name:
|
|
||||||
raise ValueError(f"Не удалось определить interface-name для {args['connection_id']}")
|
|
||||||
|
|
||||||
# Переподключаем устройство
|
|
||||||
devices = await nm.get_devices()
|
|
||||||
for device_path in devices:
|
|
||||||
device = NetworkDeviceGeneric(device_path, self.bus)
|
|
||||||
if await device.interface == interface_name:
|
if await device.interface == interface_name:
|
||||||
await device.disconnect()
|
await device.disconnect()
|
||||||
print(f"Отключён от {interface_name}")
|
print(f"Отключён от {interface_name}")
|
||||||
await nm.activate_connection(connection=connection_paths[0], device=device_path)
|
await nm.activate_connection(connection=connection_paths[0],
|
||||||
|
device=device_path)
|
||||||
print(f"Переподключён к {args['connection_id']}")
|
print(f"Переподключён к {args['connection_id']}")
|
||||||
return
|
return
|
||||||
|
|
||||||
raise RuntimeError(f"Устройство для интерфейса {interface_name} не найдено")
|
raise RuntimeError(
|
||||||
|
f"Устройство для интерфейса {interface_name} не найдено")
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from sdbus_async.networkmanager import (
|
||||||
NetworkManagerSettings,
|
NetworkManagerSettings,
|
||||||
NetworkDeviceWireless,
|
NetworkDeviceWireless,
|
||||||
AccessPoint,
|
AccessPoint,
|
||||||
DeviceType
|
DeviceType,
|
||||||
)
|
)
|
||||||
from sdbus import sd_bus_open_system
|
from sdbus import sd_bus_open_system
|
||||||
from models.network import (
|
from models.network import (
|
||||||
|
|
@ -17,10 +17,11 @@ from models.network import (
|
||||||
IPConfig,
|
IPConfig,
|
||||||
IndividualIPConfig,
|
IndividualIPConfig,
|
||||||
NetworkInterfacesContainer,
|
NetworkInterfacesContainer,
|
||||||
WiFiDevice
|
WiFiDevice,
|
||||||
)
|
)
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
class NetworkService:
|
class NetworkService:
|
||||||
"""Сервис для взаимодействия с NetworkManager через D-Bus"""
|
"""Сервис для взаимодействия с NetworkManager через D-Bus"""
|
||||||
|
|
||||||
|
|
@ -29,76 +30,99 @@ class NetworkService:
|
||||||
self.nm = NetworkManager(self.system_bus)
|
self.nm = NetworkManager(self.system_bus)
|
||||||
self.nm_settings = NetworkManagerSettings(self.system_bus)
|
self.nm_settings = NetworkManagerSettings(self.system_bus)
|
||||||
|
|
||||||
async def get_ip_method(self, device: NetworkDeviceGeneric, ip_type: str) -> str:
|
async def get_ip_method(self, device: NetworkDeviceGeneric,
|
||||||
|
ip_type: str) -> str:
|
||||||
"""Получает метод IP для устройства"""
|
"""Получает метод IP для устройства"""
|
||||||
try:
|
try:
|
||||||
config = await device.get_applied_connection()
|
config = await device.get_applied_connection()
|
||||||
settings = config[0].get(ip_type, {})
|
settings = config[0].get(ip_type, {})
|
||||||
return str(settings.get("method", ('s', 'unknown'))[1])
|
return str(settings.get("method", ("s", "unknown"))[1])
|
||||||
except Exception:
|
except Exception:
|
||||||
return 'unknown'
|
return "unknown"
|
||||||
|
|
||||||
async def get_connection_name(self, device: NetworkDeviceGeneric) -> str:
|
async def get_connection_name(self, device: NetworkDeviceGeneric) -> str:
|
||||||
"""Извлекает имя соединения (GENERAL.CONNECTION) для устройства"""
|
"""Извлекает имя соединения (GENERAL.CONNECTION) для устройства"""
|
||||||
try:
|
try:
|
||||||
applied_connection = await device.get_applied_connection()
|
applied_connection = await device.get_applied_connection()
|
||||||
connection_settings = applied_connection[0]
|
connection_settings = applied_connection[0]
|
||||||
connection_info = connection_settings.get('connection', {})
|
connection_info = connection_settings.get("connection", {})
|
||||||
return str(connection_info.get('id', ('s', 'unknown'))[1])
|
return str(connection_info.get("id", ("s", "unknown"))[1])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# print(f"Ошибка при получении соединения для устройства {await device.interface}: {e}")
|
# print(f"Ошибка при получении соединения для устройства {await device.interface}: {e}")
|
||||||
# срёт ошибки дл отключенных интерфейсов
|
# срёт ошибки дл отключенных интерфейсов
|
||||||
# добавить потом условия для отключенных интерфейсов
|
# добавить потом условия для отключенных интерфейсов
|
||||||
return 'unknown'
|
return "unknown"
|
||||||
|
|
||||||
async def get_ip_config(self, device: NetworkDeviceGeneric, ip_version: str) -> List[IndividualIPConfig]:
|
async def get_ip_config(self, device: NetworkDeviceGeneric,
|
||||||
|
ip_version: str) -> List[IndividualIPConfig]:
|
||||||
"""Получает конфигурацию IP для устройства"""
|
"""Получает конфигурацию IP для устройства"""
|
||||||
configs = []
|
configs = []
|
||||||
device_ip = await getattr(device, f'ip{ip_version}_config')
|
device_ip = await getattr(device, f"ip{ip_version}_config")
|
||||||
if device_ip != '/':
|
if device_ip != "/":
|
||||||
ip_config = globals()[f'IPv{ip_version}Config'](device_ip, self.system_bus)
|
ip_config = globals()[f"IPv{ip_version}Config"](device_ip,
|
||||||
|
self.system_bus)
|
||||||
address_data = await ip_config.address_data
|
address_data = await ip_config.address_data
|
||||||
for address in address_data:
|
for address in address_data:
|
||||||
config = IndividualIPConfig(
|
config = IndividualIPConfig(
|
||||||
address=str(address['address'][1]),
|
address=str(address["address"][1]),
|
||||||
routes=[
|
routes=[
|
||||||
globals()[f'IPv{ip_version}Route'](
|
globals()[f"IPv{ip_version}Route"](
|
||||||
destination=route.get('dest', ('s', '0.0.0.0' if ip_version == '4' else '::'))[1],
|
destination=route.get(
|
||||||
prefix=route.get('prefix', ('u', 0))[1],
|
"dest",
|
||||||
next_hop=route.get('next-hop', ('s', '0.0.0.0' if ip_version == '4' else '::'))[1],
|
("s",
|
||||||
metric=route.get('metric', ('u', 0))[1]
|
"0.0.0.0" if ip_version == "4" else "::"))[1],
|
||||||
|
prefix=route.get("prefix", ("u", 0))[1],
|
||||||
|
next_hop=route.get(
|
||||||
|
"next-hop",
|
||||||
|
("s",
|
||||||
|
"0.0.0.0" if ip_version == "4" else "::"),
|
||||||
|
)[1],
|
||||||
|
metric=route.get("metric", ("u", 0))[1],
|
||||||
) for route in await ip_config.route_data
|
) for route in await ip_config.route_data
|
||||||
],
|
],
|
||||||
method=await self.get_ip_method(device, f'ipv{ip_version}'),
|
method=await self.get_ip_method(device,
|
||||||
|
f"ipv{ip_version}"),
|
||||||
gateway=str(await ip_config.gateway),
|
gateway=str(await ip_config.gateway),
|
||||||
dns=[str(ns['address'][1]) if ip_version == '4' else str(ns) for ns in await getattr(ip_config, 'nameserver_data' if ip_version == '4' else 'nameservers')]
|
dns=[
|
||||||
|
str(ns["address"][1]) if ip_version == "4" else str(ns)
|
||||||
|
for ns in await getattr(
|
||||||
|
ip_config,
|
||||||
|
"nameserver_data" if ip_version ==
|
||||||
|
"4" else "nameservers",
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
configs.append(config)
|
configs.append(config)
|
||||||
return configs
|
return configs
|
||||||
|
|
||||||
async def get_wifi_info(self, wifi_device: NetworkDeviceWireless) -> Dict[str, Optional[Union[str, int]]]:
|
async def get_wifi_info(
|
||||||
|
self, wifi_device: NetworkDeviceWireless
|
||||||
|
) -> Dict[str, Optional[Union[str, int]]]:
|
||||||
"""Получает информацию о Wi-Fi устройстве"""
|
"""Получает информацию о Wi-Fi устройстве"""
|
||||||
active_ap = await wifi_device.active_access_point
|
active_ap = await wifi_device.active_access_point
|
||||||
wifi_info = {
|
wifi_info = {
|
||||||
'ssid': None,
|
"ssid": None,
|
||||||
'bssid': None,
|
"bssid": None,
|
||||||
'bitrate': await wifi_device.bitrate,
|
"bitrate": await wifi_device.bitrate,
|
||||||
'strength': None,
|
"strength": None,
|
||||||
'frequency': None,
|
"frequency": None,
|
||||||
'mode': None
|
"mode": None,
|
||||||
}
|
}
|
||||||
if active_ap != '/':
|
if active_ap != "/":
|
||||||
ap = AccessPoint(active_ap, self.system_bus)
|
ap = AccessPoint(active_ap, self.system_bus)
|
||||||
wifi_info.update({
|
wifi_info.update({
|
||||||
'ssid': await ap.ssid,
|
"ssid": await ap.ssid,
|
||||||
'bssid': await ap.hw_address,
|
"bssid": await ap.hw_address,
|
||||||
'strength': await ap.strength,
|
"strength": await ap.strength,
|
||||||
'frequency': await ap.frequency,
|
"frequency": await ap.frequency,
|
||||||
'mode': await ap.mode
|
"mode": await ap.mode,
|
||||||
})
|
})
|
||||||
return wifi_info
|
return wifi_info
|
||||||
|
|
||||||
async def get_network_interfaces(self, initial_positions: Dict[str, Tuple[int, int]] = None) -> List[Union[NetworkInterface, WiFiDevice]]:
|
async def get_network_interfaces(
|
||||||
|
self,
|
||||||
|
initial_positions: Dict[str, Tuple[int, int]] = None
|
||||||
|
) -> List[Union[NetworkInterface, WiFiDevice]]:
|
||||||
"""Получает список сетевых интерфейсов"""
|
"""Получает список сетевых интерфейсов"""
|
||||||
devices_paths = await self.nm.get_devices()
|
devices_paths = await self.nm.get_devices()
|
||||||
network_interfaces = []
|
network_interfaces = []
|
||||||
|
|
@ -109,30 +133,44 @@ class NetworkService:
|
||||||
interface_name = str(await generic_device.interface)
|
interface_name = str(await generic_device.interface)
|
||||||
device_type = DeviceType(await generic_device.device_type).name
|
device_type = DeviceType(await generic_device.device_type).name
|
||||||
|
|
||||||
if device_type == 'LOOPBACK':
|
if device_type == "LOOPBACK":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
device_state = str(await generic_device.state)
|
device_state = str(await generic_device.state)
|
||||||
if device_state == '10':
|
if device_state == "10":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
common_data = {
|
common_data = {
|
||||||
'name': interface_name,
|
"name":
|
||||||
'type': device_type,
|
interface_name,
|
||||||
'device_state': device_state,
|
"type":
|
||||||
'active_connection': await self.get_connection_name(generic_device),
|
device_type,
|
||||||
'mac_address': str(await generic_device.hw_address),
|
"device_state":
|
||||||
'device_mtu': str(await generic_device.mtu),
|
device_state,
|
||||||
'ipv4': IPConfig(configurations=await self.get_ip_config(generic_device, '4')),
|
"active_connection":
|
||||||
'ipv6': IPConfig(configurations=await self.get_ip_config(generic_device, '6')),
|
await self.get_connection_name(generic_device),
|
||||||
'x': initial_positions.get(interface_name, (100, 100))[0],
|
"mac_address":
|
||||||
'y': initial_positions.get(interface_name, (100, 100))[1],
|
str(await generic_device.hw_address),
|
||||||
|
"device_mtu":
|
||||||
|
str(await generic_device.mtu),
|
||||||
|
"ipv4":
|
||||||
|
IPConfig(configurations=await self.get_ip_config(
|
||||||
|
generic_device, "4")),
|
||||||
|
"ipv6":
|
||||||
|
IPConfig(configurations=await self.get_ip_config(
|
||||||
|
generic_device, "6")),
|
||||||
|
"x":
|
||||||
|
initial_positions.get(interface_name, (100, 100))[0],
|
||||||
|
"y":
|
||||||
|
initial_positions.get(interface_name, (100, 100))[1],
|
||||||
}
|
}
|
||||||
|
|
||||||
if device_type == "WIFI":
|
if device_type == "WIFI":
|
||||||
wifi_device = NetworkDeviceWireless(device_path, self.system_bus)
|
wifi_device = NetworkDeviceWireless(device_path,
|
||||||
|
self.system_bus)
|
||||||
wifi_info = await self.get_wifi_info(wifi_device)
|
wifi_info = await self.get_wifi_info(wifi_device)
|
||||||
network_interfaces.append(WiFiDevice(**common_data, **wifi_info))
|
network_interfaces.append(
|
||||||
|
WiFiDevice(**common_data, **wifi_info))
|
||||||
else:
|
else:
|
||||||
network_interfaces.append(NetworkInterface(**common_data))
|
network_interfaces.append(NetworkInterface(**common_data))
|
||||||
|
|
||||||
|
|
@ -141,7 +179,10 @@ class NetworkService:
|
||||||
async def get_hostname(self) -> Dict[str, str]:
|
async def get_hostname(self) -> Dict[str, str]:
|
||||||
"""Возвращает имя хоста из команды hostname в формате JSON"""
|
"""Возвращает имя хоста из команды hostname в формате JSON"""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(['hostname'], capture_output=True, text=True, check=True)
|
result = subprocess.run(["hostname"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True)
|
||||||
hostname = result.stdout.strip()
|
hostname = result.stdout.strip()
|
||||||
return {"hostname": hostname}
|
return {"hostname": hostname}
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
|
|
@ -149,52 +190,83 @@ class NetworkService:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"Error processing hostname output: {e}")
|
raise RuntimeError(f"Error processing hostname output: {e}")
|
||||||
|
|
||||||
async def get_interfaces_by_state(self, state: str, initial_positions: Dict[str, Tuple[int, int]] = None) -> List[NetworkInterface]:
|
async def get_interfaces_by_state(
|
||||||
|
self,
|
||||||
|
state: str,
|
||||||
|
initial_positions: Dict[str, Tuple[int, int]] = None
|
||||||
|
) -> List[NetworkInterface]:
|
||||||
"""
|
"""
|
||||||
Получает список интерфейсов по состоянию
|
Получает список интерфейсов по состоянию
|
||||||
"""
|
"""
|
||||||
all_interfaces = await self.get_network_interfaces(initial_positions)
|
all_interfaces = await self.get_network_interfaces(initial_positions)
|
||||||
return [interface for interface in all_interfaces if interface.device_state == state]
|
return [
|
||||||
|
interface for interface in all_interfaces
|
||||||
|
if interface.device_state == state
|
||||||
|
]
|
||||||
|
|
||||||
async def get_interfaces_container(self, state: Optional[str] = None, initial_positions: Dict[str, Tuple[int, int]] = None, container_x: int = 0, container_y: int = 0) -> NetworkInterfacesContainer:
|
async def get_interfaces_container(
|
||||||
""" Получает объект, содержащий список интерфейсов и координаты контейнера """
|
self,
|
||||||
|
state: Optional[str] = None,
|
||||||
|
initial_positions: Dict[str, Tuple[int, int]] = None,
|
||||||
|
container_x: int = 0,
|
||||||
|
container_y: int = 0,
|
||||||
|
) -> NetworkInterfacesContainer:
|
||||||
|
"""Получает объект, содержащий список интерфейсов и координаты контейнера"""
|
||||||
if state:
|
if state:
|
||||||
interfaces = await self.get_interfaces_by_state(state, initial_positions)
|
interfaces = await self.get_interfaces_by_state(
|
||||||
|
state, initial_positions)
|
||||||
else:
|
else:
|
||||||
interfaces = await self.get_network_interfaces(initial_positions)
|
interfaces = await self.get_network_interfaces(initial_positions)
|
||||||
return NetworkInterfacesContainer(
|
return NetworkInterfacesContainer(interfaces=interfaces,
|
||||||
interfaces=interfaces,
|
|
||||||
container_x=container_x,
|
container_x=container_x,
|
||||||
container_y=container_y
|
container_y=container_y)
|
||||||
)
|
|
||||||
|
|
||||||
async def get_network_interfaces_container(self, initial_positions: Optional[Dict[str, Tuple[int, int]]] = None, ) -> NetworkInterfacesContainer:
|
async def get_network_interfaces_container(
|
||||||
""" Получает контейнер с сетевыми интерфейсами и координатами контейнера """
|
self,
|
||||||
|
initial_positions: Optional[Dict[str, Tuple[int, int]]] = None,
|
||||||
|
) -> NetworkInterfacesContainer:
|
||||||
|
"""Получает контейнер с сетевыми интерфейсами и координатами контейнера"""
|
||||||
interfaces = await self.get_network_interfaces(initial_positions)
|
interfaces = await self.get_network_interfaces(initial_positions)
|
||||||
return NetworkInterfacesContainer(
|
return NetworkInterfacesContainer(interfaces=interfaces,
|
||||||
interfaces=interfaces,
|
|
||||||
container_x=0,
|
container_x=0,
|
||||||
container_y=0
|
container_y=0)
|
||||||
)
|
|
||||||
|
|
||||||
async def get_activated_interfaces(self, initial_positions: Dict[str, Tuple[int, int]] = None) -> List[NetworkInterface]:
|
async def get_activated_interfaces(
|
||||||
""" Получает список активных сетевых интерфейсов """
|
self,
|
||||||
return await self.get_interfaces_by_state('100', initial_positions)
|
initial_positions: Dict[str, Tuple[int, int]] = None
|
||||||
|
) -> List[NetworkInterface]:
|
||||||
|
"""Получает список активных сетевых интерфейсов"""
|
||||||
|
return await self.get_interfaces_by_state("100", initial_positions)
|
||||||
|
|
||||||
async def get_deactivated_interfaces(self, initial_positions: Dict[str, Tuple[int, int]] = None) -> List[NetworkInterface]:
|
async def get_deactivated_interfaces(
|
||||||
""" Получает список неактивных сетевых интерфейсов """
|
self,
|
||||||
|
initial_positions: Dict[str, Tuple[int, int]] = None
|
||||||
|
) -> List[NetworkInterface]:
|
||||||
|
"""Получает список неактивных сетевых интерфейсов"""
|
||||||
all_interfaces = await self.get_network_interfaces(initial_positions)
|
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,
|
||||||
return await self.get_interfaces_container('100', initial_positions, container_x, container_y)
|
initial_positions: Dict[str, Tuple[int, int]] = None,
|
||||||
|
container_x: int = 0,
|
||||||
|
container_y: int = 0,
|
||||||
|
) -> NetworkInterfacesContainer:
|
||||||
|
"""Получает объект, содержащий список активных интерфейсов и координаты контейнера"""
|
||||||
|
return await self.get_interfaces_container("100", initial_positions,
|
||||||
|
container_x, container_y)
|
||||||
|
|
||||||
async def get_deactivated_interfaces_container(self, initial_positions: Dict[str, Tuple[int, int]] = None, container_x: int = 0, container_y: int = 0) -> NetworkInterfacesContainer:
|
async def get_deactivated_interfaces_container(
|
||||||
""" Получает объект, содержащий список неактивных интерфейсов и координаты контейнера """
|
self,
|
||||||
|
initial_positions: Dict[str, Tuple[int, int]] = None,
|
||||||
|
container_x: int = 0,
|
||||||
|
container_y: int = 0,
|
||||||
|
) -> NetworkInterfacesContainer:
|
||||||
|
"""Получает объект, содержащий список неактивных интерфейсов и координаты контейнера"""
|
||||||
interfaces = await self.get_deactivated_interfaces(initial_positions)
|
interfaces = await self.get_deactivated_interfaces(initial_positions)
|
||||||
return NetworkInterfacesContainer(
|
return NetworkInterfacesContainer(interfaces=interfaces,
|
||||||
interfaces=interfaces,
|
|
||||||
container_x=container_x,
|
container_x=container_x,
|
||||||
container_y=container_y
|
container_y=container_y)
|
||||||
)
|
|
||||||
|
|
|
||||||
424
SRV/services/subnet_service.py
Normal file
424
SRV/services/subnet_service.py
Normal file
|
|
@ -0,0 +1,424 @@
|
||||||
|
from scapy.config import conf
|
||||||
|
from mac_vendor_lookup import MacLookup
|
||||||
|
from typing import List, Optional, Tuple, Dict
|
||||||
|
import socket
|
||||||
|
import logging
|
||||||
|
import netifaces
|
||||||
|
import ipaddress
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import time
|
||||||
|
from functools import lru_cache
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from models.subnet import SubnetScanResult, Device
|
||||||
|
from config.ports import port_config
|
||||||
|
from scapy.layers.inet import IP, TCP
|
||||||
|
from scapy.layers.l2 import ARP, Ether
|
||||||
|
from scapy.sendrecv import srp, sr1
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class NetworkScanner:
|
||||||
|
def __init__(self, interface: str):
|
||||||
|
self.interface = interface
|
||||||
|
conf.iface = interface
|
||||||
|
|
||||||
|
def scan(self, subnet: str) -> List[Tuple[str, str]]:
|
||||||
|
try:
|
||||||
|
arp = ARP(pdst=subnet)
|
||||||
|
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
|
||||||
|
packet = ether / arp
|
||||||
|
result = srp(packet,
|
||||||
|
timeout=2,
|
||||||
|
verbose=False,
|
||||||
|
iface=self.interface)[0]
|
||||||
|
return [(received.psrc, received.hwsrc)
|
||||||
|
for sent, received in result]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка ARP-сканирования подсети {subnet}: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
class PortScanner:
|
||||||
|
def __init__(self, interface: str):
|
||||||
|
self.interface = interface
|
||||||
|
|
||||||
|
def scan(self, ip: str) -> List[str]:
|
||||||
|
open_ports = []
|
||||||
|
|
||||||
|
def check_port(port: int) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
pkt = sr1(
|
||||||
|
IP(dst=ip) / TCP(dport=port, flags="S"),
|
||||||
|
timeout=2,
|
||||||
|
verbose=False,
|
||||||
|
)
|
||||||
|
if pkt and pkt.haslayer(TCP) and pkt[TCP].flags == 0x12:
|
||||||
|
return port_config.get_port_name(port)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(
|
||||||
|
f"Ошибка проверки порта {port} для IP {ip}: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
max_workers = min(len(port_config.get_ports()), 15)
|
||||||
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||||
|
results = executor.map(check_port, port_config.get_ports())
|
||||||
|
open_ports = [port for port in results if port is not None]
|
||||||
|
logger.debug(f"Открытые порты для IP {ip}: {open_ports}")
|
||||||
|
return open_ports
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Ошибка при сканировании портов для IP {ip}: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
class DeviceInfoCollector:
|
||||||
|
def __init__(self):
|
||||||
|
self.mac_lookup = MacLookup()
|
||||||
|
self.nbtscan_cache = {}
|
||||||
|
self.nbtscan_results = {}
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1000)
|
||||||
|
def get_vendor(self, mac: str) -> str:
|
||||||
|
try:
|
||||||
|
return self.mac_lookup.lookup(mac)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Не удалось определить производителя для MAC {mac}: {str(e)}")
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
def scan_nbtscan(self, subnet: str) -> None:
|
||||||
|
if subnet not in self.nbtscan_results:
|
||||||
|
try:
|
||||||
|
logger.debug(f"Запуск nbtscan для подсети {subnet}")
|
||||||
|
result = subprocess.run(["sudo", "nbtscan", "-r", str(subnet)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
timeout=10)
|
||||||
|
logger.debug(f"Полный вывод nbtscan: {result.stdout}")
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
logger.debug(f"Обработка строки: {line}")
|
||||||
|
if not line.startswith(("IP address", "-", "Doing NBT")):
|
||||||
|
parts = [p for p in line.split() if p]
|
||||||
|
if len(parts) >= 2:
|
||||||
|
ip_addr, hostname = parts[0], parts[1]
|
||||||
|
if hostname != "<unknown>":
|
||||||
|
self.nbtscan_cache[ip_addr] = hostname
|
||||||
|
logger.info(f"NetBIOS: Найден хостнейм {hostname} для IP {ip_addr}")
|
||||||
|
self.nbtscan_results[subnet] = True
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error(f"NetBIOS: Таймаут для подсети {subnet}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"NetBIOS: Ошибка для подсети {subnet}: {str(e)}")
|
||||||
|
|
||||||
|
def get_hostname(self, ip: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
socket.setdefaulttimeout(2)
|
||||||
|
return socket.gethostbyaddr(ip)[0]
|
||||||
|
except (socket.herror, socket.gaierror, socket.timeout):
|
||||||
|
logger.debug(f"DNS: Не удалось разрешить имя для IP {ip}")
|
||||||
|
|
||||||
|
subnet = ipaddress.ip_network(f"{ip}/24", strict=False)
|
||||||
|
if subnet not in self.nbtscan_results:
|
||||||
|
self.scan_nbtscan(str(subnet))
|
||||||
|
|
||||||
|
if ip in self.nbtscan_cache:
|
||||||
|
return self.nbtscan_cache[ip]
|
||||||
|
|
||||||
|
logger.debug(f"Не удалось определить хостнейм для IP {ip} через DNS или NetBIOS")
|
||||||
|
return None
|
||||||
|
|
||||||
|
class DeviceStorage:
|
||||||
|
def __init__(self, storage_path: str = "etc/gonets/SRV/storage/devices/devices.json"):
|
||||||
|
self.storage_file = Path(storage_path)
|
||||||
|
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.offline_timeout = timedelta(days=7)
|
||||||
|
|
||||||
|
def load_devices(self) -> Dict[str, Device]:
|
||||||
|
try:
|
||||||
|
if self.storage_file.exists():
|
||||||
|
with open(self.storage_file, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
devices = {}
|
||||||
|
for device_data in data.get("devices", []):
|
||||||
|
device = Device(**device_data)
|
||||||
|
if device.mac:
|
||||||
|
devices[device.mac] = device
|
||||||
|
return self._clean_old_devices(devices)
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при загрузке устройств: {str(e)}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _clean_old_devices(self, devices: Dict[str, Device]) -> Dict[str, Device]:
|
||||||
|
now = datetime.now()
|
||||||
|
filtered = {}
|
||||||
|
for mac, device in devices.items():
|
||||||
|
if device.status == "offline" and device.last_seen:
|
||||||
|
try:
|
||||||
|
last_seen = datetime.fromisoformat(device.last_seen)
|
||||||
|
if now - last_seen <= self.offline_timeout:
|
||||||
|
filtered[mac] = device
|
||||||
|
else:
|
||||||
|
logger.info(f"Удалено устройство {mac} (offline > 7 дней)")
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"Неверный формат last_seen для устройства {mac}")
|
||||||
|
else:
|
||||||
|
filtered[mac] = device
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
def save_devices(self, devices: List[Device]) -> None:
|
||||||
|
try:
|
||||||
|
data = SubnetScanResult(devices=devices).model_dump()
|
||||||
|
with open(self.storage_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
logger.info(f"Сохранено {len(devices)} устройств в {self.storage_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при сохранении устройств: {str(e)}")
|
||||||
|
|
||||||
|
class SubnetService:
|
||||||
|
def __init__(self):
|
||||||
|
self.interface = self._get_default_interface()
|
||||||
|
self.network_scanner = NetworkScanner(self.interface)
|
||||||
|
self.port_scanner = PortScanner(self.interface)
|
||||||
|
self.device_info = DeviceInfoCollector()
|
||||||
|
self.storage = DeviceStorage()
|
||||||
|
|
||||||
|
def _get_default_interface(self) -> str:
|
||||||
|
try:
|
||||||
|
gateways = netifaces.gateways()
|
||||||
|
default_gateway = gateways.get("default", {}).get(netifaces.AF_INET)
|
||||||
|
if default_gateway:
|
||||||
|
return default_gateway[1]
|
||||||
|
interfaces = netifaces.interfaces()
|
||||||
|
for iface in interfaces:
|
||||||
|
if iface != "lo":
|
||||||
|
return iface
|
||||||
|
return "eth0"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Не удалось определить интерфейс: {str(e)}")
|
||||||
|
return "eth0"
|
||||||
|
|
||||||
|
def scan_subnet(self, subnet: str, ports: List[int] = None) -> SubnetScanResult:
|
||||||
|
start_time = time.time()
|
||||||
|
devices = []
|
||||||
|
now = datetime.now().astimezone()
|
||||||
|
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
|
||||||
|
try:
|
||||||
|
saved_devices = self.storage.load_devices()
|
||||||
|
|
||||||
|
local_device = self._get_local_device(subnet)
|
||||||
|
if local_device:
|
||||||
|
local_device.status = "online"
|
||||||
|
local_device.last_seen = formatted_now
|
||||||
|
devices.append(local_device)
|
||||||
|
if local_device.mac:
|
||||||
|
saved_devices[local_device.mac] = local_device
|
||||||
|
|
||||||
|
discovered_devices = self.network_scanner.scan(subnet)
|
||||||
|
self.device_info.scan_nbtscan(subnet)
|
||||||
|
|
||||||
|
def process_device(ip_mac: Tuple[str, str]) -> Optional[Device]:
|
||||||
|
ip, mac = ip_mac
|
||||||
|
if local_device and ip == local_device.ip:
|
||||||
|
return None
|
||||||
|
|
||||||
|
existing_device = saved_devices.get(mac)
|
||||||
|
if existing_device:
|
||||||
|
return Device(
|
||||||
|
ip=ip,
|
||||||
|
mac=mac,
|
||||||
|
hostname=self.device_info.get_hostname(ip) or existing_device.hostname,
|
||||||
|
vendor=self.device_info.get_vendor(mac),
|
||||||
|
open_ports=self.port_scanner.scan(ip) or existing_device.open_ports,
|
||||||
|
status="online",
|
||||||
|
last_seen=formatted_now,
|
||||||
|
note=existing_device.note,
|
||||||
|
)
|
||||||
|
vendor = self.device_info.get_vendor(mac)
|
||||||
|
hostname = self.device_info.get_hostname(ip)
|
||||||
|
open_ports = self.port_scanner.scan(ip)
|
||||||
|
return Device(
|
||||||
|
ip=ip,
|
||||||
|
mac=mac,
|
||||||
|
hostname=hostname,
|
||||||
|
vendor=vendor,
|
||||||
|
open_ports=open_ports,
|
||||||
|
status="online",
|
||||||
|
last_seen=formatted_now,
|
||||||
|
note=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||||
|
device_results = executor.map(process_device, discovered_devices)
|
||||||
|
devices.extend([d for d in device_results if d is not None])
|
||||||
|
|
||||||
|
active_macs = {d.mac for d in devices if d.mac}
|
||||||
|
for mac, device in saved_devices.items():
|
||||||
|
if mac not in active_macs:
|
||||||
|
device.status = "offline"
|
||||||
|
devices.append(device)
|
||||||
|
|
||||||
|
devices.sort(key=lambda x: int(ipaddress.IPv4Address(x.ip)))
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
if device.mac:
|
||||||
|
saved_devices[device.mac] = device
|
||||||
|
self.storage.save_devices(list(saved_devices.values()))
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Сканирование подсети {subnet} завершено за {time.time() - start_time:.2f} секунд, "
|
||||||
|
f"найдено {len(devices)} устройств (активных: {len(active_macs)})"
|
||||||
|
)
|
||||||
|
return SubnetScanResult(devices=devices)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при сканировании подсети {subnet}: {str(e)}")
|
||||||
|
return SubnetScanResult(devices=devices)
|
||||||
|
|
||||||
|
def _get_local_device(self, subnet: str) -> Optional[Device]:
|
||||||
|
now = datetime.now().astimezone()
|
||||||
|
formatted_now = now.strftime("%d-%m-%Y, %H:%M")
|
||||||
|
try:
|
||||||
|
subnet_network = ipaddress.ip_network(subnet, strict=False)
|
||||||
|
interfaces = netifaces.interfaces()
|
||||||
|
for iface in interfaces:
|
||||||
|
if iface == "lo":
|
||||||
|
continue
|
||||||
|
addrs = netifaces.ifaddresses(iface)
|
||||||
|
if netifaces.AF_INET in addrs and netifaces.AF_LINK in addrs:
|
||||||
|
ip = addrs[netifaces.AF_INET][0]["addr"]
|
||||||
|
if ipaddress.ip_address(ip) in subnet_network:
|
||||||
|
mac = addrs[netifaces.AF_LINK][0]["addr"]
|
||||||
|
vendor = self.device_info.get_vendor(mac)
|
||||||
|
hostname = self.device_info.get_hostname(ip)
|
||||||
|
open_ports = self.port_scanner.scan(ip)
|
||||||
|
logger.debug(
|
||||||
|
f"Локальное устройство: IP={ip}, MAC={mac}, Ports={open_ports}, Hostname={hostname}"
|
||||||
|
)
|
||||||
|
return Device(
|
||||||
|
ip=ip,
|
||||||
|
mac=mac,
|
||||||
|
hostname=hostname,
|
||||||
|
vendor=vendor,
|
||||||
|
open_ports=open_ports,
|
||||||
|
status="online",
|
||||||
|
last_seen=formatted_now,
|
||||||
|
note=None,
|
||||||
|
)
|
||||||
|
logger.warning(
|
||||||
|
f"Локальное устройство не найдено в подсети {subnet}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Не удалось получить локальное устройство: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_note(self, mac: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Добавляет заметку к устройству по MAC-адресу.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac (str): MAC-адрес устройства.
|
||||||
|
note (str): Текст заметки.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||||
|
- Успех: (True, None).
|
||||||
|
- Ошибка: (False, сообщение об ошибке).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Если произошла ошибка при сохранении.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
saved_devices = self.storage.load_devices()
|
||||||
|
if mac not in saved_devices:
|
||||||
|
logger.warning(f"Устройство с MAC {mac} не найдено")
|
||||||
|
return False, f"Устройство с MAC {mac} не найдено"
|
||||||
|
if saved_devices[mac].note is not None:
|
||||||
|
logger.error(
|
||||||
|
f"Заметка для устройства с MAC {mac} уже существует")
|
||||||
|
return False, f"Заметка для устройства с MAC {mac} уже существует"
|
||||||
|
saved_devices[mac].note = note
|
||||||
|
self.storage.save_devices(list(saved_devices.values()))
|
||||||
|
logger.info(
|
||||||
|
f"Добавлена заметка '{note}' для устройства с MAC {mac}")
|
||||||
|
return True, None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Ошибка при добавлении заметки для MAC {mac}: {str(e)}")
|
||||||
|
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||||
|
|
||||||
|
def update_note(self, mac: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Обновляет заметку устройства по MAC-адресу.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac (str): MAC-адрес устройства.
|
||||||
|
note (str): Новый текст заметки.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||||
|
- Успех: (True, None).
|
||||||
|
- Ошибка: (False, сообщение об ошибке).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Если произошла ошибка при сохранении.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
saved_devices = self.storage.load_devices()
|
||||||
|
if mac not in saved_devices:
|
||||||
|
logger.warning(f"Устройство с MAC {mac} не найдено")
|
||||||
|
return False, f"Устройство с MAC {mac} не найдено"
|
||||||
|
if saved_devices[mac].note is None:
|
||||||
|
logger.error(
|
||||||
|
f"Заметка для устройства с MAC {mac} не существует")
|
||||||
|
return False, f"Заметка для устройства с MAC {mac} не существует"
|
||||||
|
saved_devices[mac].note = note
|
||||||
|
self.storage.save_devices(list(saved_devices.values()))
|
||||||
|
logger.info(
|
||||||
|
f"Обновлена заметка '{note}' для устройства с MAC {mac}")
|
||||||
|
return True, None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Ошибка при обновлении заметки для MAC {mac}: {str(e)}")
|
||||||
|
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||||
|
|
||||||
|
def delete_note(self, mac: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Удаляет заметку устройства по MAC-адресу.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac (str): MAC-адрес устройства.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||||
|
- Успех: (True, None).
|
||||||
|
- Ошибка: (False, сообщение об ошибке).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Если произошла ошибка при сохранении.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
saved_devices = self.storage.load_devices()
|
||||||
|
if mac not in saved_devices:
|
||||||
|
logger.warning(f"Устройство с MAC {mac} не найдено")
|
||||||
|
return False, f"Устройство с MAC {mac} не найдено"
|
||||||
|
if saved_devices[mac].note is None:
|
||||||
|
logger.error(
|
||||||
|
f"Заметка для устройства с MAC {mac} не существует")
|
||||||
|
return False, f"Заметка для устройства с MAC {mac} не существует"
|
||||||
|
saved_devices[mac].note = None
|
||||||
|
self.storage.save_devices(list(saved_devices.values()))
|
||||||
|
logger.info(f"Удалена заметка для устройства с MAC {mac}")
|
||||||
|
return True, None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Ошибка при удалении заметки для MAC {mac}: {str(e)}")
|
||||||
|
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||||
|
|
@ -41,9 +41,9 @@ class UFWService:
|
||||||
print(f"Command error: {result.stderr}")
|
print(f"Command error: {result.stderr}")
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise subprocess.CalledProcessError(
|
raise subprocess.CalledProcessError(result.returncode, command,
|
||||||
result.returncode, command, result.stdout, result.stderr
|
result.stdout,
|
||||||
)
|
result.stderr)
|
||||||
|
|
||||||
return result.stdout
|
return result.stdout
|
||||||
|
|
||||||
|
|
@ -116,9 +116,8 @@ class UFWService:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
status_result = self._run_command("ufw status")
|
status_result = self._run_command("ufw status")
|
||||||
first_line = (
|
first_line = (status_result.splitlines()[0]
|
||||||
status_result.splitlines()[0] if status_result.splitlines() else ""
|
if status_result.splitlines() else "")
|
||||||
)
|
|
||||||
is_enabled = "Status: active" in first_line
|
is_enabled = "Status: active" in first_line
|
||||||
|
|
||||||
message = "UFW активен" if is_enabled else "UFW неактивен"
|
message = "UFW активен" if is_enabled else "UFW неактивен"
|
||||||
|
|
@ -166,7 +165,9 @@ class UFWService:
|
||||||
print(f"Ошибка при выключении UFW: {e}")
|
print(f"Ошибка при выключении UFW: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def add_rules_base(self, rules: List[UFWRule], direction: str = "") -> bool:
|
def add_rules_base(self,
|
||||||
|
rules: List[UFWRule],
|
||||||
|
direction: str = "") -> bool:
|
||||||
"""
|
"""
|
||||||
Базовый метод для добавления правил UFW.
|
Базовый метод для добавления правил UFW.
|
||||||
|
|
||||||
|
|
@ -296,7 +297,10 @@ class UFWService:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Ошибка выполнения команды: {str(e)}"
|
error_msg = f"Ошибка выполнения команды: {str(e)}"
|
||||||
print(error_msg)
|
print(error_msg)
|
||||||
results.append({"success": False, "message": error_msg})
|
results.append({
|
||||||
|
"success": False,
|
||||||
|
"message": error_msg
|
||||||
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Ошибка при добавлении правила: {str(e)}"
|
error_msg = f"Ошибка при добавлении правила: {str(e)}"
|
||||||
|
|
@ -334,7 +338,7 @@ class UFWService:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Извлекаем номер правила из строки [N]
|
# Извлекаем номер правила из строки [N]
|
||||||
rule_num = int(line[1 : line.index("]")])
|
rule_num = int(line[1:line.index("]")])
|
||||||
rules.append(rule_num)
|
rules.append(rule_num)
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
continue
|
continue
|
||||||
|
|
@ -349,32 +353,37 @@ class UFWService:
|
||||||
try:
|
try:
|
||||||
# Проверяем существует ли правило с таким номером
|
# Проверяем существует ли правило с таким номером
|
||||||
if rule_number not in rules:
|
if rule_number not in rules:
|
||||||
results.append(
|
results.append({
|
||||||
{
|
"number":
|
||||||
"number": rule_number,
|
rule_number,
|
||||||
"success": False,
|
"success":
|
||||||
"error": f"Правило с номером {rule_number} не найдено",
|
False,
|
||||||
}
|
"error":
|
||||||
)
|
f"Правило с номером {rule_number} не найдено",
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Удаляем правило
|
# Удаляем правило
|
||||||
self._run_command(f"ufw --force delete {rule_number}")
|
self._run_command(f"ufw --force delete {rule_number}")
|
||||||
|
|
||||||
results.append(
|
results.append({
|
||||||
{
|
"number":
|
||||||
"number": rule_number,
|
rule_number,
|
||||||
"success": True,
|
"success":
|
||||||
"message": f"Правило {rule_number} успешно удалено",
|
True,
|
||||||
}
|
"message":
|
||||||
)
|
f"Правило {rule_number} успешно удалено",
|
||||||
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
print(f"Ошибка при удалении правила {rule_number}: {e}")
|
print(
|
||||||
results.append(
|
f"Ошибка при удалении правила {rule_number}: {e}")
|
||||||
{"number": rule_number, "success": False, "error": str(e)}
|
results.append({
|
||||||
)
|
"number": rule_number,
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
})
|
||||||
|
|
||||||
# Подсчитываем статистику
|
# Подсчитываем статистику
|
||||||
total = len(rule_numbers)
|
total = len(rule_numbers)
|
||||||
|
|
|
||||||
392
SRV/services/wifi_service.py
Normal file
392
SRV/services/wifi_service.py
Normal file
|
|
@ -0,0 +1,392 @@
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import List, Optional, Dict, Tuple
|
||||||
|
from mac_vendor_lookup import MacLookup
|
||||||
|
import pywifi
|
||||||
|
from pywifi import Profile
|
||||||
|
from models.wifi import WifiAccessPoint, WifiScanResult
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG,
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class WifiStorage:
|
||||||
|
"""Класс для хранения данных о Wi-Fi сетях и их заметках по SSID."""
|
||||||
|
|
||||||
|
def __init__(self, storage_path: str = "etc/gonets/SRV/storage/wifi/wifi.json"):
|
||||||
|
self.storage_file = Path(storage_path)
|
||||||
|
self.storage_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def load_notes(self) -> Dict[str, Optional[str]]:
|
||||||
|
"""Загружает заметки по SSID из файла хранения."""
|
||||||
|
try:
|
||||||
|
if self.storage_file.exists():
|
||||||
|
with open(self.storage_file, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return {item["ssid"]: item.get("note") for item in data.get("networks", [])}
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при загрузке заметок: {str(e)}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_notes(self, notes: Dict[str, Optional[str]]) -> None:
|
||||||
|
"""Сохраняет заметки по SSID в файл."""
|
||||||
|
try:
|
||||||
|
data = {"networks": [{"ssid": ssid, "note": note} for ssid, note in notes.items()]}
|
||||||
|
with open(self.storage_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
logger.info(f"Сохранено {len(notes)} заметок в {self.storage_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при сохранении заметок: {str(e)}")
|
||||||
|
|
||||||
|
class WifiService:
|
||||||
|
"""Сервис для сканирования текущих Wi-Fi точек доступа"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.mac_lookup = MacLookup()
|
||||||
|
self.wifi = pywifi.PyWiFi()
|
||||||
|
self.interface = self._get_wifi_interface()
|
||||||
|
self.storage = WifiStorage()
|
||||||
|
|
||||||
|
def _get_wifi_interface(self) -> Optional[object]:
|
||||||
|
"""Автоматически определяет активный Wi-Fi-интерфейс"""
|
||||||
|
try:
|
||||||
|
interfaces = self.wifi.interfaces()
|
||||||
|
if not interfaces:
|
||||||
|
logger.error("Не найдено сетевых интерфейсов")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Доступные интерфейсы: {[iface.name() for iface in interfaces]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for iface in interfaces:
|
||||||
|
iface_name = iface.name()
|
||||||
|
if "p2p" in iface_name.lower():
|
||||||
|
logger.debug(f"Пропущен интерфейс {iface_name} (P2P)")
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
iface.scan()
|
||||||
|
time.sleep(1)
|
||||||
|
results = iface.scan_results()
|
||||||
|
if results:
|
||||||
|
logger.info(f"Выбран Wi-Fi-интерфейс: {iface_name}")
|
||||||
|
return iface
|
||||||
|
logger.debug(
|
||||||
|
f"Интерфейс {iface_name} не вернул результатов сканирования"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Интерфейс {iface_name} неактивен: {str(e)}")
|
||||||
|
|
||||||
|
logger.error("Не найден активный Wi-Fi-интерфейс")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при определении Wi-Fi-интерфейса: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1000)
|
||||||
|
def get_vendor(self, mac: str) -> str:
|
||||||
|
try:
|
||||||
|
return self.mac_lookup.lookup(mac)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Не удалось определить производителя для MAC {mac}: {str(e)}")
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
def _get_encryption_from_iw(self, ap_info: Dict) -> str:
|
||||||
|
"""Определяет шифрование из данных iw"""
|
||||||
|
security = ap_info.get("security", "")
|
||||||
|
logger.debug(
|
||||||
|
f"Шифрование для BSSID {ap_info.get('bssid')}: {security}")
|
||||||
|
if "WPA3" in security:
|
||||||
|
return "WPA3"
|
||||||
|
elif "WPA2" in security or "RSN" in security:
|
||||||
|
return "WPA2"
|
||||||
|
elif "WPA" in security:
|
||||||
|
return "WPA"
|
||||||
|
elif "WEP" in security:
|
||||||
|
return "WEP"
|
||||||
|
return "Open"
|
||||||
|
|
||||||
|
def _get_channel(self, frequency: Optional[int]) -> Optional[int]:
|
||||||
|
"""Преобразует частоту в канал."""
|
||||||
|
if frequency is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
if 2412 <= frequency <= 2472:
|
||||||
|
return (frequency - 2412) // 5 + 1
|
||||||
|
elif 5180 <= frequency <= 5825:
|
||||||
|
return (frequency - 5180) // 5 + 36
|
||||||
|
logger.debug(f"Частота {frequency} вне диапазона 2.4/5 GHz")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(
|
||||||
|
f"Ошибка при определении канала для частоты {frequency}: {str(e)}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_frequency(self, frequency: Optional[int]) -> Optional[str]:
|
||||||
|
"""Определяет частотный диапазон."""
|
||||||
|
if frequency is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
if 2412 <= frequency <= 2472:
|
||||||
|
return "2.4 GHz"
|
||||||
|
elif 5180 <= frequency <= 5825:
|
||||||
|
return "5 GHz"
|
||||||
|
logger.debug(f"Частота {frequency} вне диапазона 2.4/5 GHz")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(
|
||||||
|
f"Ошибка при определении диапазона для частоты {frequency}: {str(e)}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_iw_scan_data(self) -> List[Dict]:
|
||||||
|
"""Получает данные сканирования через iw."""
|
||||||
|
try:
|
||||||
|
if not self.interface:
|
||||||
|
logger.error("Интерфейс не определён")
|
||||||
|
return []
|
||||||
|
cmd = ["sudo", "iw", "dev", self.interface.name(), "scan"]
|
||||||
|
logger.debug(f"Выполняем команду: {' '.join(cmd)}")
|
||||||
|
result = subprocess.run(cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10)
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error(f"Ошибка iw scan: {result.stderr}")
|
||||||
|
return []
|
||||||
|
output = result.stdout
|
||||||
|
logger.debug(
|
||||||
|
f"Вывод iw scan (первые 500 символов): {output[:500]}...")
|
||||||
|
|
||||||
|
aps = []
|
||||||
|
current_ap = None
|
||||||
|
bssid_pattern = re.compile(r"BSS ([0-9a-f:]{17})")
|
||||||
|
ssid_pattern = re.compile(r"SSID: ?(.*)")
|
||||||
|
freq_pattern = re.compile(r"freq: (\d+)")
|
||||||
|
signal_pattern = re.compile(r"signal: ([-\d.]+) dBm")
|
||||||
|
rsn_pattern = re.compile(r"RSN:")
|
||||||
|
wpa_pattern = re.compile(r"WPA:")
|
||||||
|
security_type_pattern = re.compile(r"\* (WPA3|WPA2|WPA|WEP)")
|
||||||
|
|
||||||
|
for line in output.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if bssid_match := bssid_pattern.match(line):
|
||||||
|
if current_ap:
|
||||||
|
aps.append(current_ap)
|
||||||
|
current_ap = {
|
||||||
|
"bssid": bssid_match.group(1).lower(),
|
||||||
|
"security": ""
|
||||||
|
}
|
||||||
|
elif current_ap:
|
||||||
|
if ssid_match := ssid_pattern.match(line):
|
||||||
|
current_ap["ssid"] = (ssid_match.group(1)
|
||||||
|
if ssid_match.group(1).strip()
|
||||||
|
else "")
|
||||||
|
elif freq_match := freq_pattern.match(line):
|
||||||
|
current_ap["frequency"] = int(freq_match.group(1))
|
||||||
|
elif signal_match := signal_pattern.match(line):
|
||||||
|
current_ap["signal"] = float(signal_match.group(1))
|
||||||
|
elif rsn_pattern.match(line):
|
||||||
|
current_ap["security"] += "RSN;"
|
||||||
|
elif wpa_pattern.match(line):
|
||||||
|
current_ap["security"] += "WPA;"
|
||||||
|
elif security_type_match := security_type_pattern.search(
|
||||||
|
line):
|
||||||
|
current_ap["security"] += security_type_match.group(
|
||||||
|
1) + ";"
|
||||||
|
|
||||||
|
if current_ap:
|
||||||
|
aps.append(current_ap)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Найдено {len(aps)} точек через iw: {[ap['bssid'] for ap in aps]}"
|
||||||
|
)
|
||||||
|
for ap in aps:
|
||||||
|
logger.debug(f"Данные точки {ap['bssid']}: {ap}")
|
||||||
|
return aps
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logger.error(f"Ошибка при выполнении iw scan: {e.stderr}")
|
||||||
|
return []
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error("Таймаут при выполнении iw scan")
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при парсинге iw scan: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def scan_wifi(self, timeout: int = 30) -> WifiScanResult:
|
||||||
|
"""Сканирует текущие Wi-Fi точки доступа"""
|
||||||
|
start_time = time.time()
|
||||||
|
aps = []
|
||||||
|
|
||||||
|
if not self.interface:
|
||||||
|
logger.error("Wi-Fi-интерфейс не доступен")
|
||||||
|
return WifiScanResult(access_points=aps)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(
|
||||||
|
f"Начало сканирования Wi-Fi на интерфейсе {self.interface.name()}"
|
||||||
|
)
|
||||||
|
self.interface.scan()
|
||||||
|
time.sleep(10)
|
||||||
|
py_results = self.interface.scan_results()
|
||||||
|
logger.debug(f"Найдено {len(py_results)} точек через pywifi")
|
||||||
|
|
||||||
|
iw_results = self._get_iw_scan_data()
|
||||||
|
iw_by_bssid = {ap["bssid"]: ap for ap in iw_results if ap.get("bssid")}
|
||||||
|
|
||||||
|
best_aps_by_ssid: Dict[str, WifiAccessPoint] = {}
|
||||||
|
saved_notes = self.storage.load_notes()
|
||||||
|
|
||||||
|
for ap in py_results:
|
||||||
|
bssid = ap.bssid.lower() if ap.bssid else None
|
||||||
|
if not bssid:
|
||||||
|
logger.debug("Пропущена точка без BSSID")
|
||||||
|
continue
|
||||||
|
ssid = ap.ssid if ap.ssid else ""
|
||||||
|
hidden = not ssid
|
||||||
|
signal = ap.signal if ap.signal else None
|
||||||
|
iw_data = iw_by_bssid.get(bssid, {})
|
||||||
|
|
||||||
|
frequency = iw_data.get("frequency")
|
||||||
|
channel = self._get_channel(frequency)
|
||||||
|
freq_band = self._get_frequency(frequency)
|
||||||
|
encryption = self._get_encryption_from_iw(iw_data)
|
||||||
|
vendor = self.get_vendor(bssid)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Точка {bssid}: freq={frequency}, channel={channel}, encryption={encryption}, signal={signal}"
|
||||||
|
)
|
||||||
|
|
||||||
|
current_ap = WifiAccessPoint(
|
||||||
|
ssid=ssid,
|
||||||
|
bssid=bssid,
|
||||||
|
channel=channel,
|
||||||
|
signal=signal if signal else iw_data.get("signal"),
|
||||||
|
encryption=encryption,
|
||||||
|
frequency=freq_band,
|
||||||
|
vendor=vendor,
|
||||||
|
hidden=hidden,
|
||||||
|
)
|
||||||
|
|
||||||
|
if ssid not in best_aps_by_ssid or (
|
||||||
|
current_ap.signal and best_aps_by_ssid[ssid].signal
|
||||||
|
and current_ap.signal > best_aps_by_ssid[ssid].signal):
|
||||||
|
# При обновлении или добавлении точки сохраняем заметку из saved_notes
|
||||||
|
current_ap.note = saved_notes.get(ssid)
|
||||||
|
best_aps_by_ssid[ssid] = current_ap
|
||||||
|
|
||||||
|
aps = list(best_aps_by_ssid.values())
|
||||||
|
# Сохраняем заметки для всех найденных SSID
|
||||||
|
notes_to_save = {ap.ssid: ap.note for ap in aps if ap.ssid}
|
||||||
|
self.storage.save_notes(notes_to_save)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Сканирование Wi-Fi завершено за {time.time() - start_time:.2f} секунд, "
|
||||||
|
f"найдено {len(aps)} уникальных сетей")
|
||||||
|
return WifiScanResult(access_points=aps)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при сканировании Wi-Fi: {str(e)}")
|
||||||
|
return WifiScanResult(access_points=aps)
|
||||||
|
|
||||||
|
def add_note(self, ssid: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Добавляет заметку к сети Wi-Fi по SSID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ssid (str): SSID сети Wi-Fi.
|
||||||
|
note (str): Текст заметки.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||||
|
- Успех: (True, None).
|
||||||
|
- Ошибка: (False, сообщение об ошибке).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Если произошла ошибка при сохранении.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
saved_notes = self.storage.load_notes()
|
||||||
|
if ssid not in saved_notes or saved_notes[ssid] is None:
|
||||||
|
logger.info(f"Добавлена новая заметка для сети с SSID {ssid}")
|
||||||
|
elif saved_notes[ssid] is not None:
|
||||||
|
logger.error(f"Заметка для сети с SSID {ssid} уже существует")
|
||||||
|
return False, f"Заметка для сети с SSID {ssid} уже существует"
|
||||||
|
saved_notes[ssid] = note
|
||||||
|
self.storage.save_notes(saved_notes)
|
||||||
|
logger.info(f"Добавлена заметка '{note}' для сети с SSID {ssid}")
|
||||||
|
return True, None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при добавлении заметки для SSID {ssid}: {str(e)}")
|
||||||
|
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||||
|
|
||||||
|
def update_note(self, ssid: str, note: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Обновляет заметку сети Wi-Fi по SSID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ssid (str): SSID сети Wi-Fi.
|
||||||
|
note (str): Новый текст заметки.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||||
|
- Успех: (True, None).
|
||||||
|
- Ошибка: (False, сообщение об ошибке).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Если произошла ошибка при сохранении.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
saved_notes = self.storage.load_notes()
|
||||||
|
if ssid not in saved_notes:
|
||||||
|
logger.warning(f"Сеть с SSID {ssid} не найдена")
|
||||||
|
return False, f"Сеть с SSID {ssid} не найдена"
|
||||||
|
if saved_notes[ssid] is None:
|
||||||
|
logger.error(f"Заметка для сети с SSID {ssid} не существует")
|
||||||
|
return False, f"Заметка для сети с SSID {ssid} не существует"
|
||||||
|
saved_notes[ssid] = note
|
||||||
|
self.storage.save_notes(saved_notes)
|
||||||
|
logger.info(f"Обновлена заметка '{note}' для сети с SSID {ssid}")
|
||||||
|
return True, None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при обновлении заметки для SSID {ssid}: {str(e)}")
|
||||||
|
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||||
|
|
||||||
|
def delete_note(self, ssid: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
"""Удаляет заметку сети Wi-Fi по SSID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ssid (str): SSID сети Wi-Fi.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[bool, Optional[str]]: Кортеж (успех, сообщение об ошибке).
|
||||||
|
- Успех: (True, None).
|
||||||
|
- Ошибка: (False, сообщение об ошибке).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: Если произошла ошибка при сохранении.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
saved_notes = self.storage.load_notes()
|
||||||
|
if ssid not in saved_notes:
|
||||||
|
logger.warning(f"Сеть с SSID {ssid} не найдена")
|
||||||
|
return False, f"Сеть с SSID {ssid} не найдена"
|
||||||
|
if saved_notes[ssid] is None:
|
||||||
|
logger.error(f"Заметка для сети с SSID {ssid} не существует")
|
||||||
|
return False, f"Заметка для сети с SSID {ssid} не существует"
|
||||||
|
saved_notes[ssid] = None
|
||||||
|
self.storage.save_notes(saved_notes)
|
||||||
|
logger.info(f"Удалена заметка для сети с SSID {ssid}")
|
||||||
|
return True, None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при удалении заметки для SSID {ssid}: {str(e)}")
|
||||||
|
return False, f"Внутренняя ошибка сервера: {str(e)}"
|
||||||
|
|
@ -9,6 +9,7 @@ from models.auth import UserCreate, UserResponse
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class JsonStorage:
|
class JsonStorage:
|
||||||
"""Хранилище пользователей в JSON-файле"""
|
"""Хранилище пользователей в JSON-файле"""
|
||||||
|
|
||||||
|
|
@ -22,7 +23,8 @@ class JsonStorage:
|
||||||
try:
|
try:
|
||||||
logger.info(f"Проверка существования файла {self.file_path}")
|
logger.info(f"Проверка существования файла {self.file_path}")
|
||||||
if not os.path.exists(self.file_path):
|
if not os.path.exists(self.file_path):
|
||||||
logger.info(f"Создание директории {os.path.dirname(self.file_path)}")
|
logger.info(
|
||||||
|
f"Создание директории {os.path.dirname(self.file_path)}")
|
||||||
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
|
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
|
||||||
logger.info(f"Создание файла {self.file_path}")
|
logger.info(f"Создание файла {self.file_path}")
|
||||||
with open(self.file_path, "w") as f:
|
with open(self.file_path, "w") as f:
|
||||||
|
|
@ -32,7 +34,9 @@ class JsonStorage:
|
||||||
else:
|
else:
|
||||||
logger.info(f"Файл {self.file_path} уже существует")
|
logger.info(f"Файл {self.file_path} уже существует")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _read_data(self) -> dict:
|
def _read_data(self) -> dict:
|
||||||
|
|
@ -41,10 +45,14 @@ class JsonStorage:
|
||||||
with open(self.file_path, "r") as f:
|
with open(self.file_path, "r") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.info(f"Файл {self.file_path} не существует, возвращаем пустую структуру")
|
logger.info(
|
||||||
|
f"Файл {self.file_path} не существует, возвращаем пустую структуру"
|
||||||
|
)
|
||||||
return {"users": [], "next_id": 1}
|
return {"users": [], "next_id": 1}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _write_data(self, data: dict):
|
def _write_data(self, data: dict):
|
||||||
|
|
@ -53,7 +61,9 @@ class JsonStorage:
|
||||||
with open(self.file_path, "w") as f:
|
with open(self.file_path, "w") as f:
|
||||||
json.dump(data, f, indent=2)
|
json.dump(data, f, indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def create_user(self, user: UserCreate) -> UserResponse:
|
def create_user(self, user: UserCreate) -> UserResponse:
|
||||||
|
|
@ -70,22 +80,31 @@ class JsonStorage:
|
||||||
"id": next_id,
|
"id": next_id,
|
||||||
"username": user.username,
|
"username": user.username,
|
||||||
"password_hash": password_hash,
|
"password_hash": password_hash,
|
||||||
"is_admin": user.is_admin
|
"is_admin": user.is_admin,
|
||||||
}
|
}
|
||||||
users.append(new_user)
|
users.append(new_user)
|
||||||
data["next_id"] = next_id + 1
|
data["next_id"] = next_id + 1
|
||||||
self._write_data(data)
|
self._write_data(data)
|
||||||
logger.info(f"Пользователь {user.username} успешно зарегистрирован")
|
logger.info(
|
||||||
return UserResponse(id=new_user["id"], username=new_user["username"], is_admin=new_user["is_admin"])
|
f"Пользователь {user.username} успешно зарегистрирован")
|
||||||
|
return UserResponse(
|
||||||
|
id=new_user["id"],
|
||||||
|
username=new_user["username"],
|
||||||
|
is_admin=new_user["is_admin"],
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при создании пользователя {user.username}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при создании пользователя {user.username}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def get_user_by_username(self, username: str) -> Optional[dict]:
|
def get_user_by_username(self, username: str) -> Optional[dict]:
|
||||||
"""Получает пользователя по имени"""
|
"""Получает пользователя по имени"""
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(self.file_path):
|
if not os.path.exists(self.file_path):
|
||||||
logger.info(f"Файл {self.file_path} не существует, пользователь {username} не найден")
|
logger.info(
|
||||||
|
f"Файл {self.file_path} не существует, пользователь {username} не найден"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
data = self._read_data()
|
data = self._read_data()
|
||||||
for user in data["users"]:
|
for user in data["users"]:
|
||||||
|
|
@ -93,9 +112,12 @@ class JsonStorage:
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при получении пользователя {username}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при получении пользователя {username}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
|
def verify_password(self, plain_password: str,
|
||||||
|
hashed_password: str) -> bool:
|
||||||
"""Проверяет пароль"""
|
"""Проверяет пароль"""
|
||||||
return self.pwd_context.verify(plain_password, hashed_password)
|
return self.pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
@ -9,7 +9,9 @@ from datetime import datetime, timedelta
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SessionStorage:
|
class SessionStorage:
|
||||||
|
|
||||||
def __init__(self, file_path: str = "/etc/gonets/sessions.json"):
|
def __init__(self, file_path: str = "/etc/gonets/sessions.json"):
|
||||||
self.file_path = file_path
|
self.file_path = file_path
|
||||||
self._ensure_file_exists()
|
self._ensure_file_exists()
|
||||||
|
|
@ -18,7 +20,8 @@ class SessionStorage:
|
||||||
try:
|
try:
|
||||||
logger.info(f"Проверка существования файла {self.file_path}")
|
logger.info(f"Проверка существования файла {self.file_path}")
|
||||||
if not os.path.exists(self.file_path):
|
if not os.path.exists(self.file_path):
|
||||||
logger.info(f"Создание директории {os.path.dirname(self.file_path)}")
|
logger.info(
|
||||||
|
f"Создание директории {os.path.dirname(self.file_path)}")
|
||||||
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
|
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
|
||||||
logger.info(f"Создание файла {self.file_path}")
|
logger.info(f"Создание файла {self.file_path}")
|
||||||
with open(self.file_path, "w") as f:
|
with open(self.file_path, "w") as f:
|
||||||
|
|
@ -28,7 +31,9 @@ class SessionStorage:
|
||||||
else:
|
else:
|
||||||
logger.info(f"Файл {self.file_path} уже существует")
|
logger.info(f"Файл {self.file_path} уже существует")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при создании файла {self.file_path}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _read_data(self) -> Dict:
|
def _read_data(self) -> Dict:
|
||||||
|
|
@ -36,7 +41,9 @@ class SessionStorage:
|
||||||
with open(self.file_path, "r") as f:
|
with open(self.file_path, "r") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при чтении файла {self.file_path}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _write_data(self, data: Dict):
|
def _write_data(self, data: Dict):
|
||||||
|
|
@ -44,7 +51,9 @@ class SessionStorage:
|
||||||
with open(self.file_path, "w") as f:
|
with open(self.file_path, "w") as f:
|
||||||
json.dump(data, f, indent=2)
|
json.dump(data, f, indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при записи файла {self.file_path}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def create_session(self, username: str, token: str, client_id: str) -> str:
|
def create_session(self, username: str, token: str, client_id: str) -> str:
|
||||||
|
|
@ -60,13 +69,17 @@ class SessionStorage:
|
||||||
"token": token,
|
"token": token,
|
||||||
"client_id": client_id,
|
"client_id": client_id,
|
||||||
"created_at": created_at,
|
"created_at": created_at,
|
||||||
"expiry": expiry
|
"expiry": expiry,
|
||||||
}
|
}
|
||||||
self._write_data(data)
|
self._write_data(data)
|
||||||
logger.info(f"Сессия создана для пользователя {username} с session_id {session_id}")
|
logger.info(
|
||||||
|
f"Сессия создана для пользователя {username} с session_id {session_id}"
|
||||||
|
)
|
||||||
return session_id
|
return session_id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при создании сессии для {username}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при создании сессии для {username}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def get_latest_session(self, client_id: str) -> Optional[Dict]:
|
def get_latest_session(self, client_id: str) -> Optional[Dict]:
|
||||||
|
|
@ -95,7 +108,9 @@ class SessionStorage:
|
||||||
self._write_data(data)
|
self._write_data(data)
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при получении сессии для client_id {client_id}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при получении сессии для client_id {client_id}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def delete_session(self, session_id: str):
|
def delete_session(self, session_id: str):
|
||||||
|
|
@ -108,7 +123,9 @@ class SessionStorage:
|
||||||
self._write_data(data)
|
self._write_data(data)
|
||||||
logger.info(f"Сессия с session_id {session_id} удалена")
|
logger.info(f"Сессия с session_id {session_id} удалена")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при удалении сессии {session_id}: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при удалении сессии {session_id}: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def clean_expired_sessions(self):
|
def clean_expired_sessions(self):
|
||||||
|
|
@ -127,7 +144,8 @@ class SessionStorage:
|
||||||
# Удаляем истекшие сессии
|
# Удаляем истекшие сессии
|
||||||
for session_id in expired_sessions:
|
for session_id in expired_sessions:
|
||||||
del data["sessions"][session_id]
|
del data["sessions"][session_id]
|
||||||
logger.info(f"Удалена истекшая сессия с session_id {session_id}")
|
logger.info(
|
||||||
|
f"Удалена истекшая сессия с session_id {session_id}")
|
||||||
|
|
||||||
# Записываем обновлённые данные
|
# Записываем обновлённые данные
|
||||||
if expired_sessions:
|
if expired_sessions:
|
||||||
|
|
@ -137,5 +155,7 @@ class SessionStorage:
|
||||||
logger.debug("Истекшие сессии не найдены")
|
logger.debug("Истекшие сессии не найдены")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при очистке истекших сессий: {type(e).__name__} - {str(e)}")
|
logger.error(
|
||||||
|
f"Ошибка при очистке истекших сессий: {type(e).__name__} - {str(e)}"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
gonets (0.2-5) UNRELEASED; urgency=medium
|
gonets (0.3-1) UNRELEASED; urgency=medium
|
||||||
|
|
||||||
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
||||||
|
|
||||||
-- GOSSYS LLC <devteam@gossys.dev> Thu, 08 May 2025 15:39:41 +0500
|
-- GOSSYS LLC <devteam@gossys.dev> Thu, 19 Jun 2025 13:24:42 +0500
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
Package: gonets-dbgsym
|
Package: gonets-dbgsym
|
||||||
Source: gonets
|
Source: gonets
|
||||||
Version: 0.2-5
|
Version: 0.3-1
|
||||||
Auto-Built-Package: debug-symbols
|
Auto-Built-Package: debug-symbols
|
||||||
Architecture: amd64
|
Architecture: amd64
|
||||||
Maintainer: GOSSYS LLC <devteam@gossys.dev>
|
Maintainer: GOSSYS LLC <devteam@gossys.dev>
|
||||||
Installed-Size: 1528
|
Installed-Size: 1528
|
||||||
Depends: gonets (= 0.2-5)
|
Depends: gonets (= 0.3-1)
|
||||||
Section: debug
|
Section: debug
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Description: debug symbols for gonets
|
Description: debug symbols for gonets
|
||||||
|
|
|
||||||
6
debian/README.Debian
vendored
6
debian/README.Debian
vendored
|
|
@ -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
10
debian/README.source
vendored
|
|
@ -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
4
debian/changelog
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
gonets (0.2-5) UNRELEASED; urgency=medium
|
gonets (0.3-1) UNRELEASED; urgency=medium
|
||||||
|
|
||||||
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
||||||
|
|
||||||
-- GOSSYS LLC <devteam@gossys.dev> Thu, 08 May 2025 15:39:41 +0500
|
-- GOSSYS LLC <devteam@gossys.dev> Thu, 19 Jun 2025 13:24:42 +0500
|
||||||
|
|
|
||||||
9
debian/control
vendored
9
debian/control
vendored
|
|
@ -6,7 +6,7 @@ Rules-Requires-Root: no
|
||||||
Build-Depends:
|
Build-Depends:
|
||||||
debhelper-compat (= 13),
|
debhelper-compat (= 13),
|
||||||
Standards-Version: 4.6.2
|
Standards-Version: 4.6.2
|
||||||
Homepage: <insert the upstream URL, if relevant>
|
Homepage: dev.gossys.dev
|
||||||
#Vcs-Browser: https://salsa.debian.org/debian/gonets
|
#Vcs-Browser: https://salsa.debian.org/debian/gonets
|
||||||
#Vcs-Git: https://salsa.debian.org/debian/gonets.git
|
#Vcs-Git: https://salsa.debian.org/debian/gonets.git
|
||||||
|
|
||||||
|
|
@ -14,5 +14,8 @@ Package: gonets
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Depends:
|
Depends:
|
||||||
${shlibs:Depends},
|
${shlibs:Depends},
|
||||||
${misc:Depends}
|
${misc:Depends},
|
||||||
Description: Панель администрирования Linux.
|
pip,
|
||||||
|
nbtscan,
|
||||||
|
python3.11-dev
|
||||||
|
Description: Приложение администрирования Linux.
|
||||||
|
|
|
||||||
6
debian/files
vendored
6
debian/files
vendored
|
|
@ -1,3 +1,3 @@
|
||||||
gonets-dbgsym_0.2-5_amd64.deb debug optional automatic=yes
|
gonets-dbgsym_0.3-1_amd64.deb debug optional automatic=yes
|
||||||
gonets_0.2-5_amd64.buildinfo unknown optional
|
gonets_0.3-1_amd64.buildinfo unknown optional
|
||||||
gonets_0.2-5_amd64.deb unknown optional
|
gonets_0.3-1_amd64.deb unknown optional
|
||||||
|
|
|
||||||
2
debian/gonets-docs.docs
vendored
2
debian/gonets-docs.docs
vendored
|
|
@ -1,2 +1,2 @@
|
||||||
README.source
|
|
||||||
README.Debian
|
README.Debian
|
||||||
|
README.source
|
||||||
|
|
|
||||||
10
debian/gonets/DEBIAN/control
vendored
10
debian/gonets/DEBIAN/control
vendored
|
|
@ -1,10 +1,10 @@
|
||||||
Package: gonets
|
Package: gonets
|
||||||
Version: 0.2-5
|
Version: 0.3-1
|
||||||
Architecture: amd64
|
Architecture: amd64
|
||||||
Maintainer: GOSSYS LLC <devteam@gossys.dev>
|
Maintainer: GOSSYS LLC <devteam@gossys.dev>
|
||||||
Installed-Size: 267506
|
Installed-Size: 343612
|
||||||
Depends: libasound2 (>= 1.0.17), libatk-bridge2.0-0 (>= 2.5.3), libatk1.0-0 (>= 2.32.0), libatspi2.0-0 (>= 2.9.90), libc6 (>= 2.34), libcairo2 (>= 1.6.0), libcups2 (>= 1.7.0), libdbus-1-3 (>= 1.9.14), libexpat1 (>= 2.1~beta3), libgbm1 (>= 17.1.0~rc2), libgcc-s1 (>= 3.0), libglib2.0-0 (>= 2.37.3), libgtk-3-0 (>= 3.9.10), libnspr4 (>= 2:4.9-2~), libnss3 (>= 2:3.30), libpango-1.0-0 (>= 1.14.0), libudev1 (>= 183), libx11-6 (>= 2:1.4.99.1), libxcb1 (>= 1.9.2), libxcomposite1 (>= 1:0.4.5), libxdamage1 (>= 1:1.1), libxext6, libxfixes3, libxkbcommon0 (>= 0.5.0), libxrandr2
|
Depends: libasound2 (>= 1.0.17), libatk-bridge2.0-0 (>= 2.5.3), libatk1.0-0 (>= 2.32.0), libatspi2.0-0 (>= 2.9.90), libc6 (>= 2.34), libcairo2 (>= 1.6.0), libcups2 (>= 1.7.0), libdbus-1-3 (>= 1.9.14), libexpat1 (>= 2.1~beta3), libgbm1 (>= 17.1.0~rc2), libgcc-s1 (>= 3.0), libglib2.0-0 (>= 2.37.3), libgtk-3-0 (>= 3.9.10), libnspr4 (>= 2:4.9-2~), libnss3 (>= 2:3.30), libpango-1.0-0 (>= 1.14.0), libudev1 (>= 183), libx11-6 (>= 2:1.4.99.1), libxcb1 (>= 1.9.2), libxcomposite1 (>= 1:0.4.5), libxdamage1 (>= 1:1.1), libxext6, libxfixes3, libxkbcommon0 (>= 0.5.0), libxrandr2, pip, nbtscan, python3.11-dev
|
||||||
Section: unknown
|
Section: unknown
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Homepage: <insert the upstream URL, if relevant>
|
Homepage: dev.gossys.dev
|
||||||
Description: Панель администрирования Linux.
|
Description: Приложение администрирования Linux.
|
||||||
|
|
|
||||||
42346
debian/gonets/DEBIAN/md5sums
vendored
42346
debian/gonets/DEBIAN/md5sums
vendored
File diff suppressed because it is too large
Load diff
6
debian/gonets/DEBIAN/postinst
vendored
6
debian/gonets/DEBIAN/postinst
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
mkdir -p /etc/gonets/certs
|
||||||
|
|
||||||
pip3 install \
|
pip3 install \
|
||||||
pydbus==0.6.0 \
|
pydbus==0.6.0 \
|
||||||
|
|
@ -20,13 +21,14 @@ pip3 install \
|
||||||
passlib[bcrypt]==1.7.4 \
|
passlib[bcrypt]==1.7.4 \
|
||||||
python-multipart==0.0.20 \
|
python-multipart==0.0.20 \
|
||||||
bcrypt==4.3.0 \
|
bcrypt==4.3.0 \
|
||||||
|
mac-vendor-lookup==0.1.12 \
|
||||||
|
pywifi==1.1.12 \
|
||||||
|
manuf==1.1.5 \
|
||||||
--break-system-packages || {
|
--break-system-packages || {
|
||||||
echo "Ошибка: Не удалось установить Python-зависимости" >&2
|
echo "Ошибка: Не удалось установить Python-зависимости" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
mkdir -p /etc/gonets/certs
|
|
||||||
|
|
||||||
openssl genrsa -out /etc/gonets/certs/gonets.key 2048
|
openssl genrsa -out /etc/gonets/certs/gonets.key 2048
|
||||||
openssl req -new -key /etc/gonets/certs/gonets.key -out /etc/gonets/certs/gonets.csr -subj "/"
|
openssl req -new -key /etc/gonets/certs/gonets.key -out /etc/gonets/certs/gonets.csr -subj "/"
|
||||||
openssl x509 -req -days 365 -in /etc/gonets/certs/gonets.csr -signkey /etc/gonets/certs/gonets.key -out /etc/gonets/certs/gonets.crt
|
openssl x509 -req -days 365 -in /etc/gonets/certs/gonets.csr -signkey /etc/gonets/certs/gonets.key -out /etc/gonets/certs/gonets.crt
|
||||||
|
|
|
||||||
2
debian/gonets/DEBIAN/shlibs
vendored
2
debian/gonets/DEBIAN/shlibs
vendored
|
|
@ -1 +1 @@
|
||||||
libvulkan 1 gonets (>= 0.2)
|
libvulkan 1 gonets (>= 0.3)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue