new awesome build
This commit is contained in:
0
APP_PROFILER/services/__init__.py
Normal file
0
APP_PROFILER/services/__init__.py
Normal file
37
APP_PROFILER/services/config.py
Normal file
37
APP_PROFILER/services/config.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from models import SystemSettings, PKISetting
|
||||
from schemas import SystemSettingsUpdate, PKISettingUpdate
|
||||
|
||||
def get_system_settings(db: Session):
|
||||
settings = db.query(SystemSettings).first()
|
||||
if not settings:
|
||||
settings = SystemSettings()
|
||||
db.add(settings)
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
return settings
|
||||
|
||||
def update_system_settings(db: Session, settings_in: SystemSettingsUpdate):
|
||||
settings = get_system_settings(db)
|
||||
for key, value in settings_in.model_dump(exclude_unset=True).items():
|
||||
setattr(settings, key, value)
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
return settings
|
||||
|
||||
def get_pki_settings(db: Session):
|
||||
settings = db.query(PKISetting).first()
|
||||
if not settings:
|
||||
settings = PKISetting()
|
||||
db.add(settings)
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
return settings
|
||||
|
||||
def update_pki_settings(db: Session, settings_in: PKISettingUpdate):
|
||||
settings = get_pki_settings(db)
|
||||
for key, value in settings_in.model_dump(exclude_unset=True).items():
|
||||
setattr(settings, key, value)
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
return settings
|
||||
108
APP_PROFILER/services/generator.py
Normal file
108
APP_PROFILER/services/generator.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import os
|
||||
import logging
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from sqlalchemy.orm import Session
|
||||
from .config import get_system_settings, get_pki_settings
|
||||
from .pki import PKI_DIR
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
|
||||
|
||||
env = Environment(loader=FileSystemLoader(TEMPLATES_DIR))
|
||||
|
||||
def generate_server_config(db: Session, output_path: str = "server.conf"):
|
||||
settings = get_system_settings(db)
|
||||
pki_settings = get_pki_settings(db)
|
||||
template = env.get_template("server.conf.j2")
|
||||
|
||||
# Rendering Path
|
||||
file_ca_path = os.path.join(PKI_DIR, "ca.crt")
|
||||
file_srv_cert_path = os.path.join(PKI_DIR, "issued", f"{pki_settings.fqdn_server}.crt")
|
||||
file_srv_key_path = os.path.join(PKI_DIR, "private", f"{pki_settings.fqdn_server}.key")
|
||||
file_dh_path = os.path.join(PKI_DIR, "dh.pem")
|
||||
file_ta_path = os.path.join(PKI_DIR, "ta.key")
|
||||
|
||||
# Render template
|
||||
config_content = template.render(
|
||||
protocol=settings.protocol,
|
||||
port=settings.port,
|
||||
ca_path=file_ca_path,
|
||||
srv_cert_path=file_srv_cert_path,
|
||||
srv_key_path=file_srv_key_path,
|
||||
dh_path=file_dh_path,
|
||||
ta_path=file_ta_path,
|
||||
vpn_network=settings.vpn_network,
|
||||
vpn_netmask=settings.vpn_netmask,
|
||||
tunnel_type=settings.tunnel_type,
|
||||
split_routes=settings.split_routes,
|
||||
user_defined_dns=settings.user_defined_dns,
|
||||
dns_servers=settings.dns_servers,
|
||||
client_to_client=settings.client_to_client,
|
||||
duplicate_cn=settings.duplicate_cn,
|
||||
crl_verify=settings.crl_verify,
|
||||
user_defined_cdscripts=settings.user_defined_cdscripts,
|
||||
connect_script=settings.connect_script,
|
||||
disconnect_script=settings.disconnect_script,
|
||||
management_interface=settings.management_interface,
|
||||
management_interface_address=settings.management_interface_address,
|
||||
management_port=settings.management_port,
|
||||
tun_mtu=settings.tun_mtu,
|
||||
mssfix=settings.mssfix
|
||||
)
|
||||
|
||||
# Write to file
|
||||
with open(output_path, "w") as f:
|
||||
f.write(config_content)
|
||||
|
||||
return config_content
|
||||
|
||||
def generate_client_config(db: Session, username: str, output_path: str):
|
||||
settings = get_system_settings(db)
|
||||
pki = get_pki_settings(db)
|
||||
|
||||
# Read Certs and Keys
|
||||
# Note: filenames in easy-rsa pki structure
|
||||
# ca: pki/ca.crt
|
||||
# cert: pki/issued/<username>.crt
|
||||
# key: pki/private/<username>.key
|
||||
# ta: pki/ta.key
|
||||
|
||||
def read_file(path):
|
||||
try:
|
||||
with open(path, "r") as f:
|
||||
return f.read().strip()
|
||||
except FileNotFoundError:
|
||||
logger.error(f"File not found: {path}")
|
||||
return f"Error: {path} not found"
|
||||
|
||||
ca_cert = read_file(os.path.join(PKI_DIR, "ca.crt"))
|
||||
client_cert = read_file(os.path.join(PKI_DIR, "issued", f"{username}.crt"))
|
||||
client_key = read_file(os.path.join(PKI_DIR, "private", f"{username}.key"))
|
||||
tls_auth = read_file(os.path.join(PKI_DIR, "ta.key"))
|
||||
|
||||
# Determine Remote IP
|
||||
if settings.public_ip:
|
||||
remote_ip = settings.public_ip
|
||||
else:
|
||||
from .utils import get_public_ip
|
||||
remote_ip = get_public_ip()
|
||||
|
||||
template = env.get_template("client.ovpn.j2")
|
||||
|
||||
config_content = template.render(
|
||||
protocol=settings.protocol,
|
||||
remote_ip=remote_ip,
|
||||
port=settings.port,
|
||||
ca_cert=ca_cert,
|
||||
client_cert=client_cert,
|
||||
client_key=client_key,
|
||||
tls_auth=tls_auth,
|
||||
tun_mtu=settings.tun_mtu
|
||||
)
|
||||
|
||||
with open(output_path, "w") as f:
|
||||
f.write(config_content)
|
||||
|
||||
return config_content
|
||||
181
APP_PROFILER/services/pki.py
Normal file
181
APP_PROFILER/services/pki.py
Normal file
@@ -0,0 +1,181 @@
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
from .config import get_pki_settings
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EASY_RSA_DIR = os.path.join(os.getcwd(), "easy-rsa")
|
||||
PKI_DIR = os.path.join(EASY_RSA_DIR, "pki")
|
||||
INDEX_PATH = os.path.join(PKI_DIR, "index.txt")
|
||||
|
||||
def get_pki_index_data(db: Session = None):
|
||||
"""
|
||||
Parses easy-rsa/pki/index.txt to get certificate expiration dates.
|
||||
Returns a dict: { "common_name": datetime_expiration }
|
||||
"""
|
||||
if not os.path.exists(INDEX_PATH):
|
||||
logger.warning(f"PKI index file not found at {INDEX_PATH}")
|
||||
return {}
|
||||
|
||||
pki_data = {}
|
||||
|
||||
try:
|
||||
with open(INDEX_PATH, "r") as f:
|
||||
for line in f:
|
||||
parts = line.strip().split('\t')
|
||||
# OpenSSL index.txt format:
|
||||
# 0: Flag (V=Valid, R=Revoked, E=Expired)
|
||||
# 1: Expiration Date (YYMMDDHHMMSSZ)
|
||||
# 2: Revocation Date (Can be empty)
|
||||
# 3: Serial
|
||||
# 4: Filename (unknown, often empty)
|
||||
# 5: Distinguished Name (DN) -> /CN=username
|
||||
|
||||
if len(parts) < 6:
|
||||
continue
|
||||
|
||||
flag = parts[0]
|
||||
exp_date_str = parts[1]
|
||||
dn = parts[5]
|
||||
|
||||
if flag != 'V': # Only interested in valid certs expiration? Or all? User wants 'expired_at'.
|
||||
# Even if revoked/expired, 'expired_at' (valid_until) is still a property of the cert.
|
||||
pass
|
||||
|
||||
# Extract CN
|
||||
# dn formats: /CN=anton or /C=RU/ST=Moscow.../CN=anton
|
||||
cn = None
|
||||
for segment in dn.split('/'):
|
||||
if segment.startswith("CN="):
|
||||
cn = segment.split("=", 1)[1]
|
||||
break
|
||||
|
||||
if cn:
|
||||
# Parse Date: YYMMDDHHMMSSZ -> 250116102805Z
|
||||
# Note: OpenSSL uses 2-digit year. stored as 20YY or 19YY.
|
||||
# python strptime %y handles 2-digit years (00-68 -> 2000-2068, 69-99 -> 1969-1999)
|
||||
try:
|
||||
exp_dt = datetime.strptime(exp_date_str, "%y%m%d%H%M%SZ")
|
||||
pki_data[cn] = exp_dt
|
||||
except ValueError:
|
||||
logger.error(f"Failed to parse date: {exp_date_str}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading PKI index: {e}")
|
||||
|
||||
return pki_data
|
||||
|
||||
def _run_easyrsa(command: list, env: dict):
|
||||
env_vars = os.environ.copy()
|
||||
env_vars.update(env)
|
||||
# Ensure EASYRSA_PKI is set
|
||||
env_vars["EASYRSA_PKI"] = PKI_DIR
|
||||
|
||||
cmd = [os.path.join(EASY_RSA_DIR, "easyrsa")] + command
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env_vars,
|
||||
cwd=EASY_RSA_DIR
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"EasyRSA command failed: {cmd}")
|
||||
logger.error(f"Stderr: {result.stderr}")
|
||||
raise Exception(f"EasyRSA command failed: {result.stderr}")
|
||||
return result.stdout
|
||||
except Exception as e:
|
||||
logger.exception("Error running EasyRSA")
|
||||
raise e
|
||||
|
||||
def _get_easyrsa_env(db: Session):
|
||||
pki = get_pki_settings(db)
|
||||
env = {
|
||||
"EASYRSA_DN": pki.easyrsa_dn,
|
||||
"EASYRSA_REQ_COUNTRY": pki.easyrsa_req_country,
|
||||
"EASYRSA_REQ_PROVINCE": pki.easyrsa_req_province,
|
||||
"EASYRSA_REQ_CITY": pki.easyrsa_req_city,
|
||||
"EASYRSA_REQ_ORG": pki.easyrsa_req_org,
|
||||
"EASYRSA_REQ_EMAIL": pki.easyrsa_req_email,
|
||||
"EASYRSA_REQ_OU": pki.easyrsa_req_ou,
|
||||
"EASYRSA_KEY_SIZE": str(pki.easyrsa_key_size),
|
||||
"EASYRSA_CA_EXPIRE": str(pki.easyrsa_ca_expire),
|
||||
"EASYRSA_CERT_EXPIRE": str(pki.easyrsa_cert_expire),
|
||||
"EASYRSA_CERT_RENEW": str(pki.easyrsa_cert_renew),
|
||||
"EASYRSA_CRL_DAYS": str(pki.easyrsa_crl_days),
|
||||
"EASYRSA_BATCH": "1" if pki.easyrsa_batch else "0"
|
||||
}
|
||||
return env
|
||||
|
||||
def init_pki(db: Session):
|
||||
env = _get_easyrsa_env(db)
|
||||
pki_settings = get_pki_settings(db)
|
||||
|
||||
if os.path.exists(os.path.join(PKI_DIR, "ca.crt")):
|
||||
logger.warning("PKI already initialized")
|
||||
return "PKI already initialized"
|
||||
|
||||
# Init PKI
|
||||
_run_easyrsa(["init-pki"], env)
|
||||
|
||||
# Build CA
|
||||
_run_easyrsa(["--req-cn=" + pki_settings.fqdn_ca, "build-ca", "nopass"], env)
|
||||
|
||||
# Build Server Cert
|
||||
_run_easyrsa(["build-server-full", pki_settings.fqdn_server, "nopass"], env)
|
||||
|
||||
# Gen DH
|
||||
_run_easyrsa(["gen-dh"], env)
|
||||
|
||||
# Gen TLS Auth Key (requires openvpn, not easyrsa)
|
||||
ta_key_path = os.path.join(PKI_DIR, "ta.key")
|
||||
subprocess.run(["openvpn", "--genkey", "secret", ta_key_path], check=True)
|
||||
|
||||
# Gen CRL
|
||||
_run_easyrsa(["gen-crl"], env)
|
||||
|
||||
return "PKI Initialized"
|
||||
|
||||
def clear_pki(db: Session):
|
||||
# 1. Clear Database Users
|
||||
from models import UserProfile
|
||||
try:
|
||||
db.query(UserProfile).delete()
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to clear user profiles from DB: {e}")
|
||||
db.rollback()
|
||||
|
||||
# 2. Clear Client Configs
|
||||
client_conf_dir = os.path.join(os.getcwd(), "client-config")
|
||||
if os.path.exists(client_conf_dir):
|
||||
import shutil
|
||||
try:
|
||||
shutil.rmtree(client_conf_dir)
|
||||
# Recreate empty dir
|
||||
os.makedirs(client_conf_dir, exist_ok=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to clear client configs: {e}")
|
||||
|
||||
# 3. Clear PKI
|
||||
if os.path.exists(PKI_DIR):
|
||||
import shutil
|
||||
shutil.rmtree(PKI_DIR)
|
||||
return "System cleared: PKI environment, User DB, and Client profiles wiped."
|
||||
return "PKI directory did not exist, but User DB and Client profiles were wiped."
|
||||
|
||||
def build_client(username: str, db: Session):
|
||||
env = _get_easyrsa_env(db)
|
||||
_run_easyrsa(["build-client-full", username, "nopass"], env)
|
||||
return True
|
||||
|
||||
def revoke_client(username: str, db: Session):
|
||||
env = _get_easyrsa_env(db)
|
||||
_run_easyrsa(["revoke", username], env)
|
||||
_run_easyrsa(["gen-crl"], env)
|
||||
return True
|
||||
140
APP_PROFILER/services/process.py
Normal file
140
APP_PROFILER/services/process.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
import time
|
||||
import psutil
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_os_type():
|
||||
"""
|
||||
Simple check to distinguish Alpine from others.
|
||||
"""
|
||||
if os.path.exists("/etc/alpine-release"):
|
||||
return "alpine"
|
||||
return "debian" # default fallback to systemctl
|
||||
|
||||
def control_service(action: str):
|
||||
"""
|
||||
Action: start, stop, restart
|
||||
"""
|
||||
if action not in ["start", "stop", "restart"]:
|
||||
raise ValueError("Invalid action")
|
||||
|
||||
os_type = get_os_type()
|
||||
|
||||
cmd = []
|
||||
if os_type == "alpine":
|
||||
cmd = ["rc-service", "openvpn", action]
|
||||
else:
|
||||
cmd = ["systemctl", action, "openvpn"]
|
||||
|
||||
try:
|
||||
# Capture output to return it or log it
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Service {action} executed successfully",
|
||||
"stdout": result.stdout
|
||||
}
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Service control failed: {e.stderr}")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to {action} service",
|
||||
"stderr": e.stderr
|
||||
}
|
||||
except FileNotFoundError:
|
||||
# Happens if rc-service or systemctl is missing (e.g. dev env)
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Command not found found for OS type {os_type}"
|
||||
}
|
||||
|
||||
def get_process_stats():
|
||||
"""
|
||||
Returns dict with pid, cpu_percent, memory_mb, uptime.
|
||||
Uses psutil for robust telemetry.
|
||||
"""
|
||||
pid = None
|
||||
process = None
|
||||
|
||||
# Find the process
|
||||
try:
|
||||
# Iterate over all running processes
|
||||
for proc in psutil.process_iter(['pid', 'name']):
|
||||
try:
|
||||
if proc.info['name'] == 'openvpn':
|
||||
pid = proc.info['pid']
|
||||
process = proc
|
||||
break
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to find process via psutil: {e}")
|
||||
|
||||
if not pid or not process:
|
||||
return {
|
||||
"status": "stopped",
|
||||
"pid": None,
|
||||
"cpu_percent": 0.0,
|
||||
"memory_mb": 0.0,
|
||||
"uptime": None
|
||||
}
|
||||
|
||||
# Get Stats
|
||||
try:
|
||||
# CPU Percent
|
||||
# Increasing interval to 1.0s to ensure we capture enough ticks for accurate reading
|
||||
# especially on systems with low clock resolution (e.g. 100Hz = 10ms ticks)
|
||||
cpu = process.cpu_percent(interval=1.0)
|
||||
|
||||
# Memory (RSS)
|
||||
mem_info = process.memory_info()
|
||||
rss_mb = round(mem_info.rss / 1024 / 1024, 2)
|
||||
|
||||
# Uptime
|
||||
create_time = process.create_time()
|
||||
uptime_seconds = time.time() - create_time
|
||||
uptime_str = format_seconds(uptime_seconds)
|
||||
|
||||
return {
|
||||
"status": "running",
|
||||
"pid": pid,
|
||||
"cpu_percent": cpu,
|
||||
"memory_mb": rss_mb,
|
||||
"uptime": uptime_str
|
||||
}
|
||||
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
# Process might have died between discovery and stats
|
||||
return {
|
||||
"status": "stopped",
|
||||
"pid": None,
|
||||
"cpu_percent": 0.0,
|
||||
"memory_mb": 0.0,
|
||||
"uptime": None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get process stats: {e}")
|
||||
return {
|
||||
"status": "running",
|
||||
"pid": pid,
|
||||
"cpu_percent": 0.0,
|
||||
"memory_mb": 0.0,
|
||||
"uptime": None
|
||||
}
|
||||
|
||||
def format_seconds(seconds: float) -> str:
|
||||
seconds = int(seconds)
|
||||
days, seconds = divmod(seconds, 86400)
|
||||
hours, seconds = divmod(seconds, 3600)
|
||||
minutes, seconds = divmod(seconds, 60)
|
||||
|
||||
parts = []
|
||||
if days > 0: parts.append(f"{days}d")
|
||||
if hours > 0: parts.append(f"{hours}h")
|
||||
if minutes > 0: parts.append(f"{minutes}m")
|
||||
parts.append(f"{seconds}s")
|
||||
|
||||
return " ".join(parts)
|
||||
29
APP_PROFILER/services/utils.py
Normal file
29
APP_PROFILER/services/utils.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import requests
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_public_ip() -> str:
|
||||
"""
|
||||
Detects public IP using external services.
|
||||
Falls back to 127.0.0.1 on failure.
|
||||
"""
|
||||
services = [
|
||||
"https://api.ipify.org",
|
||||
"https://ifconfig.me/ip",
|
||||
"https://icanhazip.com",
|
||||
]
|
||||
|
||||
for service in services:
|
||||
try:
|
||||
response = requests.get(service, timeout=3)
|
||||
if response.status_code == 200:
|
||||
ip = response.text.strip()
|
||||
# Basic validation could be added here
|
||||
return ip
|
||||
except requests.RequestException:
|
||||
continue
|
||||
|
||||
logger.warning("Could not detect public IP, defaulting to 127.0.0.1")
|
||||
return "127.0.0.1"
|
||||
Reference in New Issue
Block a user