first commit
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
"""Client configuration."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Application info
|
||||
APP_NAME = "mGuard VPN Client"
|
||||
APP_VERSION = "1.0.0"
|
||||
|
||||
# Default server settings
|
||||
DEFAULT_SERVER_URL = "http://localhost:8000"
|
||||
|
||||
# OpenVPN paths
|
||||
if os.name == 'nt': # Windows
|
||||
OPENVPN_EXE = r"C:\Program Files\OpenVPN\bin\openvpn.exe"
|
||||
OPENVPN_CONFIG_DIR = Path.home() / "OpenVPN" / "config"
|
||||
else: # Linux/Mac
|
||||
OPENVPN_EXE = "/usr/sbin/openvpn"
|
||||
OPENVPN_CONFIG_DIR = Path.home() / ".openvpn"
|
||||
|
||||
# Ensure config directory exists
|
||||
OPENVPN_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Local storage
|
||||
APP_DATA_DIR = Path.home() / ".mguard-vpn"
|
||||
APP_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Settings file
|
||||
SETTINGS_FILE = APP_DATA_DIR / "settings.json"
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""mGuard VPN Client - Main Entry Point."""
|
||||
|
||||
import sys
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from config import APP_NAME
|
||||
from ui.main_window import MainWindow
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
# Enable high DPI scaling
|
||||
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
||||
)
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName(APP_NAME)
|
||||
app.setStyle("Fusion")
|
||||
|
||||
# Create and show main window
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,14 @@
|
||||
# PyQt GUI
|
||||
PyQt6==6.6.1
|
||||
|
||||
# HTTP Client
|
||||
httpx==0.26.0
|
||||
|
||||
# Configuration
|
||||
python-dotenv==1.0.0
|
||||
|
||||
# Keyring for secure credential storage
|
||||
keyring==24.3.0
|
||||
|
||||
# For Windows OpenVPN management
|
||||
pywin32==306; sys_platform == 'win32'
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Client services."""
|
||||
|
||||
from .api_client import APIClient
|
||||
from .vpn_manager import VPNManager
|
||||
|
||||
__all__ = ["APIClient", "VPNManager"]
|
||||
@@ -0,0 +1,175 @@
|
||||
"""REST API client for server communication."""
|
||||
|
||||
import httpx
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Gateway:
|
||||
"""Gateway data class."""
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str]
|
||||
location: Optional[str]
|
||||
router_type: str
|
||||
is_online: bool
|
||||
vpn_ip: Optional[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Endpoint:
|
||||
"""Endpoint data class."""
|
||||
id: int
|
||||
gateway_id: int
|
||||
name: str
|
||||
description: Optional[str]
|
||||
internal_ip: str
|
||||
port: int
|
||||
protocol: str
|
||||
application_name: Optional[str]
|
||||
|
||||
|
||||
class APIClient:
|
||||
"""REST API client for mGuard VPN Server."""
|
||||
|
||||
def __init__(self, base_url: str):
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.access_token: Optional[str] = None
|
||||
self.refresh_token: Optional[str] = None
|
||||
self.client = httpx.Client(timeout=30.0)
|
||||
|
||||
def _headers(self) -> dict:
|
||||
"""Get request headers with auth token."""
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if self.access_token:
|
||||
headers["Authorization"] = f"Bearer {self.access_token}"
|
||||
return headers
|
||||
|
||||
def _request(self, method: str, endpoint: str, **kwargs) -> dict:
|
||||
"""Make authenticated request."""
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
response = self.client.request(method, url, headers=self._headers(), **kwargs)
|
||||
response.raise_for_status()
|
||||
return response.json() if response.text else {}
|
||||
|
||||
def login(self, username: str, password: str) -> bool:
|
||||
"""Login and store tokens."""
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.base_url}/api/auth/login",
|
||||
json={"username": username, "password": password}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
self.access_token = data["access_token"]
|
||||
self.refresh_token = data["refresh_token"]
|
||||
return True
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
def logout(self):
|
||||
"""Clear stored tokens."""
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
|
||||
def refresh_access_token(self) -> bool:
|
||||
"""Refresh access token using refresh token."""
|
||||
if not self.refresh_token:
|
||||
return False
|
||||
try:
|
||||
response = self.client.post(
|
||||
f"{self.base_url}/api/auth/refresh",
|
||||
params={"refresh_token": self.refresh_token}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
self.access_token = data["access_token"]
|
||||
self.refresh_token = data["refresh_token"]
|
||||
return True
|
||||
except httpx.HTTPError:
|
||||
return False
|
||||
|
||||
def get_current_user(self) -> Optional[dict]:
|
||||
"""Get current user information."""
|
||||
try:
|
||||
return self._request("GET", "/api/auth/me")
|
||||
except httpx.HTTPError:
|
||||
return None
|
||||
|
||||
def get_gateways(self) -> list[Gateway]:
|
||||
"""Get list of accessible gateways."""
|
||||
try:
|
||||
data = self._request("GET", "/api/gateways")
|
||||
return [
|
||||
Gateway(
|
||||
id=g["id"],
|
||||
name=g["name"],
|
||||
description=g.get("description"),
|
||||
location=g.get("location"),
|
||||
router_type=g["router_type"],
|
||||
is_online=g["is_online"],
|
||||
vpn_ip=g.get("vpn_ip")
|
||||
)
|
||||
for g in data
|
||||
]
|
||||
except httpx.HTTPError:
|
||||
return []
|
||||
|
||||
def get_gateways_status(self) -> list[dict]:
|
||||
"""Get online status of all gateways."""
|
||||
try:
|
||||
return self._request("GET", "/api/gateways/status")
|
||||
except httpx.HTTPError:
|
||||
return []
|
||||
|
||||
def get_endpoints(self, gateway_id: int) -> list[Endpoint]:
|
||||
"""Get endpoints for a gateway."""
|
||||
try:
|
||||
data = self._request("GET", f"/api/endpoints/gateway/{gateway_id}")
|
||||
return [
|
||||
Endpoint(
|
||||
id=e["id"],
|
||||
gateway_id=e["gateway_id"],
|
||||
name=e["name"],
|
||||
description=e.get("description"),
|
||||
internal_ip=e["internal_ip"],
|
||||
port=e["port"],
|
||||
protocol=e["protocol"],
|
||||
application_name=e.get("application_name")
|
||||
)
|
||||
for e in data
|
||||
]
|
||||
except httpx.HTTPError:
|
||||
return []
|
||||
|
||||
def connect(self, gateway_id: int, endpoint_id: int) -> dict:
|
||||
"""Request connection to endpoint."""
|
||||
try:
|
||||
return self._request(
|
||||
"POST", "/api/connections/connect",
|
||||
json={"gateway_id": gateway_id, "endpoint_id": endpoint_id}
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def disconnect(self, connection_id: int) -> dict:
|
||||
"""Disconnect from endpoint."""
|
||||
try:
|
||||
return self._request(
|
||||
"POST", "/api/connections/disconnect",
|
||||
json={"connection_id": connection_id}
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
return {"message": str(e)}
|
||||
|
||||
def get_active_connections(self) -> list[dict]:
|
||||
"""Get list of active connections."""
|
||||
try:
|
||||
return self._request("GET", "/api/connections/active")
|
||||
except httpx.HTTPError:
|
||||
return []
|
||||
|
||||
def close(self):
|
||||
"""Close HTTP client."""
|
||||
self.client.close()
|
||||
@@ -0,0 +1,144 @@
|
||||
"""OpenVPN process management."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
from config import OPENVPN_EXE, OPENVPN_CONFIG_DIR
|
||||
|
||||
|
||||
@dataclass
|
||||
class VPNStatus:
|
||||
"""VPN connection status."""
|
||||
connected: bool
|
||||
vpn_ip: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class VPNManager:
|
||||
"""Manages OpenVPN connections."""
|
||||
|
||||
def __init__(self):
|
||||
self.process: Optional[subprocess.Popen] = None
|
||||
self.config_file: Optional[Path] = None
|
||||
self.log_file: Optional[Path] = None
|
||||
|
||||
def check_openvpn_installed(self) -> bool:
|
||||
"""Check if OpenVPN is installed."""
|
||||
if os.name == 'nt':
|
||||
return Path(OPENVPN_EXE).exists()
|
||||
else:
|
||||
try:
|
||||
subprocess.run(["which", "openvpn"], capture_output=True, check=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
def connect(self, config_content: str) -> VPNStatus:
|
||||
"""Connect using provided OpenVPN config."""
|
||||
if self.process and self.process.poll() is None:
|
||||
return VPNStatus(connected=False, error="Already connected")
|
||||
|
||||
if not self.check_openvpn_installed():
|
||||
return VPNStatus(
|
||||
connected=False,
|
||||
error="OpenVPN is not installed. Please install OpenVPN first."
|
||||
)
|
||||
|
||||
# Write config to temp file
|
||||
self.config_file = OPENVPN_CONFIG_DIR / "mguard-temp.ovpn"
|
||||
self.config_file.write_text(config_content)
|
||||
|
||||
# Log file
|
||||
self.log_file = OPENVPN_CONFIG_DIR / "mguard.log"
|
||||
|
||||
try:
|
||||
if os.name == 'nt':
|
||||
# Windows: Use OpenVPN GUI or direct call
|
||||
# Note: Requires admin privileges
|
||||
self.process = subprocess.Popen(
|
||||
[OPENVPN_EXE, "--config", str(self.config_file), "--log", str(self.log_file)],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
else:
|
||||
# Linux: Use sudo openvpn
|
||||
self.process = subprocess.Popen(
|
||||
["sudo", "openvpn", "--config", str(self.config_file), "--log", str(self.log_file)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
|
||||
# Wait a bit for connection
|
||||
import time
|
||||
time.sleep(3)
|
||||
|
||||
if self.process.poll() is not None:
|
||||
# Process ended, check for errors
|
||||
if self.log_file.exists():
|
||||
log_content = self.log_file.read_text()
|
||||
return VPNStatus(connected=False, error=f"Connection failed: {log_content[-500:]}")
|
||||
return VPNStatus(connected=False, error="Connection failed")
|
||||
|
||||
return VPNStatus(connected=True)
|
||||
|
||||
except PermissionError:
|
||||
return VPNStatus(
|
||||
connected=False,
|
||||
error="Permission denied. Run as administrator/root."
|
||||
)
|
||||
except Exception as e:
|
||||
return VPNStatus(connected=False, error=str(e))
|
||||
|
||||
def disconnect(self) -> VPNStatus:
|
||||
"""Disconnect VPN."""
|
||||
if not self.process:
|
||||
return VPNStatus(connected=False)
|
||||
|
||||
try:
|
||||
self.process.terminate()
|
||||
self.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
|
||||
self.process = None
|
||||
|
||||
# Clean up temp config
|
||||
if self.config_file and self.config_file.exists():
|
||||
self.config_file.unlink()
|
||||
|
||||
return VPNStatus(connected=False)
|
||||
|
||||
def get_status(self) -> VPNStatus:
|
||||
"""Get current VPN status."""
|
||||
if not self.process:
|
||||
return VPNStatus(connected=False)
|
||||
|
||||
if self.process.poll() is not None:
|
||||
# Process has ended
|
||||
self.process = None
|
||||
return VPNStatus(connected=False)
|
||||
|
||||
# Try to get VPN IP from log
|
||||
vpn_ip = None
|
||||
if self.log_file and self.log_file.exists():
|
||||
try:
|
||||
log_content = self.log_file.read_text()
|
||||
# Parse for IP assignment
|
||||
for line in log_content.split('\n'):
|
||||
if 'ifconfig' in line.lower() and 'netmask' in line.lower():
|
||||
parts = line.split()
|
||||
for i, part in enumerate(parts):
|
||||
if part == 'ifconfig' and i + 1 < len(parts):
|
||||
vpn_ip = parts[i + 1]
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return VPNStatus(connected=True, vpn_ip=vpn_ip)
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Check if VPN is connected."""
|
||||
return self.process is not None and self.process.poll() is None
|
||||
@@ -0,0 +1,6 @@
|
||||
"""UI components."""
|
||||
|
||||
from .main_window import MainWindow
|
||||
from .login_dialog import LoginDialog
|
||||
|
||||
__all__ = ["MainWindow", "LoginDialog"]
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Login dialog for server authentication."""
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||
QLineEdit, QPushButton, QLabel, QMessageBox, QComboBox
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from config import DEFAULT_SERVER_URL, APP_NAME
|
||||
|
||||
|
||||
class LoginDialog(QDialog):
|
||||
"""Login dialog for user authentication."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(f"{APP_NAME} - Login")
|
||||
self.setMinimumWidth(400)
|
||||
self.setModal(True)
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup UI components."""
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Title
|
||||
title = QLabel(APP_NAME)
|
||||
title.setStyleSheet("font-size: 18px; font-weight: bold; margin-bottom: 10px;")
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
|
||||
# Form
|
||||
form = QFormLayout()
|
||||
|
||||
# Server URL
|
||||
self.server_input = QComboBox()
|
||||
self.server_input.setEditable(True)
|
||||
self.server_input.addItem(DEFAULT_SERVER_URL)
|
||||
self.server_input.setCurrentText(DEFAULT_SERVER_URL)
|
||||
form.addRow("Server:", self.server_input)
|
||||
|
||||
# Username
|
||||
self.username_input = QLineEdit()
|
||||
self.username_input.setPlaceholderText("Enter username")
|
||||
form.addRow("Username:", self.username_input)
|
||||
|
||||
# Password
|
||||
self.password_input = QLineEdit()
|
||||
self.password_input.setPlaceholderText("Enter password")
|
||||
self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
form.addRow("Password:", self.password_input)
|
||||
|
||||
layout.addLayout(form)
|
||||
|
||||
# Error label
|
||||
self.error_label = QLabel()
|
||||
self.error_label.setStyleSheet("color: red;")
|
||||
self.error_label.setVisible(False)
|
||||
layout.addWidget(self.error_label)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.addStretch()
|
||||
|
||||
self.login_button = QPushButton("Login")
|
||||
self.login_button.setDefault(True)
|
||||
self.login_button.clicked.connect(self.accept)
|
||||
button_layout.addWidget(self.login_button)
|
||||
|
||||
cancel_button = QPushButton("Cancel")
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
button_layout.addWidget(cancel_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Enter key handling
|
||||
self.password_input.returnPressed.connect(self.login_button.click)
|
||||
|
||||
def get_credentials(self) -> tuple[str, str, str]:
|
||||
"""Get entered credentials."""
|
||||
return (
|
||||
self.server_input.currentText().strip(),
|
||||
self.username_input.text().strip(),
|
||||
self.password_input.text()
|
||||
)
|
||||
|
||||
def show_error(self, message: str):
|
||||
"""Show error message."""
|
||||
self.error_label.setText(message)
|
||||
self.error_label.setVisible(True)
|
||||
|
||||
def clear_error(self):
|
||||
"""Clear error message."""
|
||||
self.error_label.setVisible(False)
|
||||
@@ -0,0 +1,387 @@
|
||||
"""Main application window."""
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QSplitter, QListWidget, QListWidgetItem, QLabel,
|
||||
QPushButton, QGroupBox, QTextEdit, QStatusBar,
|
||||
QMessageBox, QProgressDialog
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QTimer
|
||||
from PyQt6.QtGui import QIcon, QColor
|
||||
|
||||
from config import APP_NAME, APP_VERSION
|
||||
from services.api_client import APIClient, Gateway, Endpoint
|
||||
from services.vpn_manager import VPNManager
|
||||
from .login_dialog import LoginDialog
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Main application window."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
|
||||
self.setMinimumSize(900, 600)
|
||||
|
||||
self.api_client: APIClient | None = None
|
||||
self.vpn_manager = VPNManager()
|
||||
self.current_gateway: Gateway | None = None
|
||||
self.current_endpoint: Endpoint | None = None
|
||||
self.current_connection_id: int | None = None
|
||||
|
||||
self._setup_ui()
|
||||
self._setup_timers()
|
||||
|
||||
# Show login dialog on start
|
||||
QTimer.singleShot(100, self._show_login)
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup UI components."""
|
||||
central = QWidget()
|
||||
self.setCentralWidget(central)
|
||||
layout = QVBoxLayout(central)
|
||||
|
||||
# Toolbar
|
||||
toolbar = QHBoxLayout()
|
||||
self.user_label = QLabel("Not logged in")
|
||||
toolbar.addWidget(self.user_label)
|
||||
toolbar.addStretch()
|
||||
|
||||
self.refresh_button = QPushButton("Refresh")
|
||||
self.refresh_button.clicked.connect(self._refresh_data)
|
||||
self.refresh_button.setEnabled(False)
|
||||
toolbar.addWidget(self.refresh_button)
|
||||
|
||||
self.logout_button = QPushButton("Logout")
|
||||
self.logout_button.clicked.connect(self._logout)
|
||||
self.logout_button.setEnabled(False)
|
||||
toolbar.addWidget(self.logout_button)
|
||||
|
||||
layout.addLayout(toolbar)
|
||||
|
||||
# Main content splitter
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
|
||||
# Gateway list
|
||||
gateway_group = QGroupBox("Gateways")
|
||||
gateway_layout = QVBoxLayout(gateway_group)
|
||||
self.gateway_list = QListWidget()
|
||||
self.gateway_list.currentItemChanged.connect(self._on_gateway_selected)
|
||||
gateway_layout.addWidget(self.gateway_list)
|
||||
splitter.addWidget(gateway_group)
|
||||
|
||||
# Endpoint list
|
||||
endpoint_group = QGroupBox("Endpoints")
|
||||
endpoint_layout = QVBoxLayout(endpoint_group)
|
||||
self.endpoint_list = QListWidget()
|
||||
self.endpoint_list.currentItemChanged.connect(self._on_endpoint_selected)
|
||||
endpoint_layout.addWidget(self.endpoint_list)
|
||||
splitter.addWidget(endpoint_group)
|
||||
|
||||
# Connection panel
|
||||
connection_group = QGroupBox("Connection")
|
||||
connection_layout = QVBoxLayout(connection_group)
|
||||
|
||||
self.gateway_info = QLabel("Select a gateway")
|
||||
connection_layout.addWidget(self.gateway_info)
|
||||
|
||||
self.endpoint_info = QLabel("Select an endpoint")
|
||||
connection_layout.addWidget(self.endpoint_info)
|
||||
|
||||
self.status_label = QLabel("Status: Disconnected")
|
||||
self.status_label.setStyleSheet("font-weight: bold;")
|
||||
connection_layout.addWidget(self.status_label)
|
||||
|
||||
connection_layout.addStretch()
|
||||
|
||||
# Connect button
|
||||
button_layout = QHBoxLayout()
|
||||
self.connect_button = QPushButton("Connect")
|
||||
self.connect_button.setEnabled(False)
|
||||
self.connect_button.clicked.connect(self._toggle_connection)
|
||||
self.connect_button.setMinimumHeight(40)
|
||||
button_layout.addWidget(self.connect_button)
|
||||
connection_layout.addLayout(button_layout)
|
||||
|
||||
# Log area
|
||||
self.log_area = QTextEdit()
|
||||
self.log_area.setReadOnly(True)
|
||||
self.log_area.setMaximumHeight(150)
|
||||
connection_layout.addWidget(self.log_area)
|
||||
|
||||
splitter.addWidget(connection_group)
|
||||
|
||||
# Set splitter sizes
|
||||
splitter.setSizes([200, 200, 400])
|
||||
layout.addWidget(splitter)
|
||||
|
||||
# Status bar
|
||||
self.status_bar = QStatusBar()
|
||||
self.setStatusBar(self.status_bar)
|
||||
self.status_bar.showMessage("Ready")
|
||||
|
||||
def _setup_timers(self):
|
||||
"""Setup periodic timers."""
|
||||
# Refresh gateway status every 30 seconds
|
||||
self.status_timer = QTimer()
|
||||
self.status_timer.timeout.connect(self._refresh_gateway_status)
|
||||
self.status_timer.setInterval(30000)
|
||||
|
||||
def _show_login(self):
|
||||
"""Show login dialog."""
|
||||
dialog = LoginDialog(self)
|
||||
|
||||
while True:
|
||||
if dialog.exec() != LoginDialog.DialogCode.Accepted:
|
||||
self.close()
|
||||
return
|
||||
|
||||
server, username, password = dialog.get_credentials()
|
||||
|
||||
if not all([server, username, password]):
|
||||
dialog.show_error("Please fill all fields")
|
||||
continue
|
||||
|
||||
# Try to connect
|
||||
self.api_client = APIClient(server)
|
||||
if self.api_client.login(username, password):
|
||||
dialog.accept()
|
||||
self._on_login_success(username)
|
||||
break
|
||||
else:
|
||||
dialog.show_error("Login failed. Check credentials.")
|
||||
|
||||
def _on_login_success(self, username: str):
|
||||
"""Handle successful login."""
|
||||
self.user_label.setText(f"Logged in as: {username}")
|
||||
self.refresh_button.setEnabled(True)
|
||||
self.logout_button.setEnabled(True)
|
||||
self.status_bar.showMessage("Connected to server")
|
||||
self._log("Connected to server")
|
||||
|
||||
# Load data
|
||||
self._refresh_data()
|
||||
|
||||
# Start status timer
|
||||
self.status_timer.start()
|
||||
|
||||
def _logout(self):
|
||||
"""Logout and show login dialog."""
|
||||
# Disconnect VPN if connected
|
||||
if self.vpn_manager.is_connected():
|
||||
self.vpn_manager.disconnect()
|
||||
|
||||
if self.api_client:
|
||||
self.api_client.logout()
|
||||
self.api_client.close()
|
||||
self.api_client = None
|
||||
|
||||
# Clear UI
|
||||
self.gateway_list.clear()
|
||||
self.endpoint_list.clear()
|
||||
self.user_label.setText("Not logged in")
|
||||
self.refresh_button.setEnabled(False)
|
||||
self.logout_button.setEnabled(False)
|
||||
self.connect_button.setEnabled(False)
|
||||
self.status_timer.stop()
|
||||
|
||||
self._log("Logged out")
|
||||
|
||||
# Show login dialog
|
||||
QTimer.singleShot(100, self._show_login)
|
||||
|
||||
def _refresh_data(self):
|
||||
"""Refresh all data from server."""
|
||||
if not self.api_client:
|
||||
return
|
||||
|
||||
self._log("Refreshing data...")
|
||||
self.status_bar.showMessage("Refreshing...")
|
||||
|
||||
# Load gateways
|
||||
gateways = self.api_client.get_gateways()
|
||||
self.gateway_list.clear()
|
||||
|
||||
for gateway in gateways:
|
||||
item = QListWidgetItem()
|
||||
item.setText(f"{gateway.name}")
|
||||
item.setData(Qt.ItemDataRole.UserRole, gateway)
|
||||
|
||||
# Set color based on status
|
||||
if gateway.is_online:
|
||||
item.setForeground(QColor("green"))
|
||||
item.setToolTip(f"Online - {gateway.location or 'No location'}")
|
||||
else:
|
||||
item.setForeground(QColor("gray"))
|
||||
item.setToolTip("Offline")
|
||||
|
||||
self.gateway_list.addItem(item)
|
||||
|
||||
self.status_bar.showMessage(f"Loaded {len(gateways)} gateways")
|
||||
self._log(f"Loaded {len(gateways)} gateways")
|
||||
|
||||
def _refresh_gateway_status(self):
|
||||
"""Refresh only gateway online status."""
|
||||
if not self.api_client:
|
||||
return
|
||||
|
||||
status_list = self.api_client.get_gateways_status()
|
||||
status_map = {s["id"]: s["is_online"] for s in status_list}
|
||||
|
||||
for i in range(self.gateway_list.count()):
|
||||
item = self.gateway_list.item(i)
|
||||
gateway: Gateway = item.data(Qt.ItemDataRole.UserRole)
|
||||
if gateway.id in status_map:
|
||||
is_online = status_map[gateway.id]
|
||||
if is_online:
|
||||
item.setForeground(QColor("green"))
|
||||
else:
|
||||
item.setForeground(QColor("gray"))
|
||||
|
||||
def _on_gateway_selected(self, current, previous):
|
||||
"""Handle gateway selection."""
|
||||
if not current:
|
||||
self.endpoint_list.clear()
|
||||
self.current_gateway = None
|
||||
self.gateway_info.setText("Select a gateway")
|
||||
return
|
||||
|
||||
gateway: Gateway = current.data(Qt.ItemDataRole.UserRole)
|
||||
self.current_gateway = gateway
|
||||
|
||||
self.gateway_info.setText(
|
||||
f"Gateway: {gateway.name}\n"
|
||||
f"Type: {gateway.router_type}\n"
|
||||
f"Status: {'Online' if gateway.is_online else 'Offline'}\n"
|
||||
f"Location: {gateway.location or 'N/A'}"
|
||||
)
|
||||
|
||||
# Load endpoints
|
||||
self._load_endpoints(gateway.id)
|
||||
|
||||
def _load_endpoints(self, gateway_id: int):
|
||||
"""Load endpoints for gateway."""
|
||||
if not self.api_client:
|
||||
return
|
||||
|
||||
endpoints = self.api_client.get_endpoints(gateway_id)
|
||||
self.endpoint_list.clear()
|
||||
|
||||
for endpoint in endpoints:
|
||||
item = QListWidgetItem()
|
||||
app_name = endpoint.application_name or "Custom"
|
||||
item.setText(f"{endpoint.name} ({app_name})")
|
||||
item.setToolTip(f"{endpoint.internal_ip}:{endpoint.port} ({endpoint.protocol})")
|
||||
item.setData(Qt.ItemDataRole.UserRole, endpoint)
|
||||
self.endpoint_list.addItem(item)
|
||||
|
||||
def _on_endpoint_selected(self, current, previous):
|
||||
"""Handle endpoint selection."""
|
||||
if not current:
|
||||
self.current_endpoint = None
|
||||
self.endpoint_info.setText("Select an endpoint")
|
||||
self.connect_button.setEnabled(False)
|
||||
return
|
||||
|
||||
endpoint: Endpoint = current.data(Qt.ItemDataRole.UserRole)
|
||||
self.current_endpoint = endpoint
|
||||
|
||||
self.endpoint_info.setText(
|
||||
f"Endpoint: {endpoint.name}\n"
|
||||
f"Address: {endpoint.internal_ip}:{endpoint.port}\n"
|
||||
f"Protocol: {endpoint.protocol.upper()}\n"
|
||||
f"Application: {endpoint.application_name or 'N/A'}"
|
||||
)
|
||||
|
||||
# Enable connect button if gateway is online
|
||||
if self.current_gateway and self.current_gateway.is_online:
|
||||
self.connect_button.setEnabled(True)
|
||||
else:
|
||||
self.connect_button.setEnabled(False)
|
||||
|
||||
def _toggle_connection(self):
|
||||
"""Toggle VPN connection."""
|
||||
if self.vpn_manager.is_connected():
|
||||
self._disconnect()
|
||||
else:
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
"""Establish VPN connection."""
|
||||
if not self.api_client or not self.current_gateway or not self.current_endpoint:
|
||||
return
|
||||
|
||||
self._log(f"Connecting to {self.current_endpoint.name}...")
|
||||
self.status_label.setText("Status: Connecting...")
|
||||
self.connect_button.setEnabled(False)
|
||||
|
||||
# Request connection from server
|
||||
result = self.api_client.connect(
|
||||
self.current_gateway.id,
|
||||
self.current_endpoint.id
|
||||
)
|
||||
|
||||
if not result.get("success"):
|
||||
self._log(f"Error: {result.get('message')}")
|
||||
self.status_label.setText("Status: Connection Failed")
|
||||
self.connect_button.setEnabled(True)
|
||||
return
|
||||
|
||||
self.current_connection_id = result.get("connection_id")
|
||||
vpn_config = result.get("vpn_config")
|
||||
|
||||
if not vpn_config:
|
||||
self._log("Error: No VPN config received")
|
||||
self.status_label.setText("Status: Error")
|
||||
self.connect_button.setEnabled(True)
|
||||
return
|
||||
|
||||
# Start VPN connection
|
||||
status = self.vpn_manager.connect(vpn_config)
|
||||
|
||||
if status.connected:
|
||||
self._log("VPN connected!")
|
||||
self.status_label.setText("Status: Connected")
|
||||
self.status_label.setStyleSheet("font-weight: bold; color: green;")
|
||||
self.connect_button.setText("Disconnect")
|
||||
self.connect_button.setEnabled(True)
|
||||
|
||||
self._log(f"Target: {result.get('target_ip')}:{result.get('target_port')}")
|
||||
else:
|
||||
self._log(f"VPN Error: {status.error}")
|
||||
self.status_label.setText("Status: VPN Failed")
|
||||
self.connect_button.setEnabled(True)
|
||||
|
||||
def _disconnect(self):
|
||||
"""Disconnect VPN."""
|
||||
self._log("Disconnecting...")
|
||||
|
||||
# Disconnect VPN
|
||||
self.vpn_manager.disconnect()
|
||||
|
||||
# Notify server
|
||||
if self.api_client and self.current_connection_id:
|
||||
self.api_client.disconnect(self.current_connection_id)
|
||||
|
||||
self.current_connection_id = None
|
||||
self.status_label.setText("Status: Disconnected")
|
||||
self.status_label.setStyleSheet("font-weight: bold; color: black;")
|
||||
self.connect_button.setText("Connect")
|
||||
self._log("Disconnected")
|
||||
|
||||
def _log(self, message: str):
|
||||
"""Add message to log area."""
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
self.log_area.append(f"[{timestamp}] {message}")
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle window close."""
|
||||
# Disconnect VPN if connected
|
||||
if self.vpn_manager.is_connected():
|
||||
self.vpn_manager.disconnect()
|
||||
|
||||
if self.api_client:
|
||||
self.api_client.close()
|
||||
|
||||
event.accept()
|
||||
Reference in New Issue
Block a user