first commit
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user