new calculation approach with unique sessions, new API endpoint to get list of active sessions, fix for UNDEF user, UI and Back to support certificate management still under development
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import sqlite3
|
||||
import configparser
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from flask import Flask, jsonify, request
|
||||
from flask import Flask, jsonify, request, send_file
|
||||
from flask_cors import CORS
|
||||
import logging
|
||||
import subprocess
|
||||
@@ -9,6 +9,10 @@ import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
from db import DatabaseManager
|
||||
from pki_manager import PKIManager
|
||||
from config_manager import ConfigManager
|
||||
from service_manager import ServiceManager
|
||||
import io
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
@@ -25,10 +29,24 @@ class OpenVPNAPI:
|
||||
self.db_manager = DatabaseManager(config_file)
|
||||
self.config = configparser.ConfigParser()
|
||||
self.config.read(config_file)
|
||||
|
||||
# Paths
|
||||
self.certificates_path = self.config.get('certificates', 'certificates_path', fallback='/etc/openvpn/certs')
|
||||
self.certificates_path = self.config.get('certificates', 'certificates_path', fallback='/etc/openvpn/certs')
|
||||
self.easyrsa_path = self.config.get('pki', 'easyrsa_path', fallback='/etc/openvpn/easy-rsa')
|
||||
self.pki_path = self.config.get('pki', 'pki_path', fallback='/etc/openvpn/pki') # Fixed default to match Settings
|
||||
self.templates_path = self.config.get('api', 'templates_path', fallback='templates')
|
||||
self.server_config_dir = self.config.get('server', 'config_dir', fallback='/etc/openvpn')
|
||||
self.server_config_path = self.config.get('server', 'config_path', fallback=os.path.join(self.server_config_dir, 'server.conf')) # Specific file
|
||||
self.public_ip = self.config.get('openvpn_monitor', 'public_ip', fallback='')
|
||||
|
||||
self.cert_extensions = self.config.get('certificates', 'certificate_extensions', fallback='crt,pem,key').split(',')
|
||||
self._cert_cache = {} # Cache structure: {filepath: {'mtime': float, 'data': dict}}
|
||||
self._cert_cache = {}
|
||||
|
||||
# Managers
|
||||
self.pki = PKIManager(self.easyrsa_path, self.pki_path)
|
||||
self.conf_mgr = ConfigManager(self.templates_path, self.server_config_dir)
|
||||
self.conf_mgr.server_conf_path = Path(self.server_config_path) # Override with specific path
|
||||
self.service = ServiceManager('openvpn') # Or openvpn@server for systemd multi-instance
|
||||
|
||||
def get_db_connection(self):
|
||||
"""Get a database connection"""
|
||||
@@ -60,30 +78,70 @@ class OpenVPNAPI:
|
||||
except Exception: return 'N/A'
|
||||
|
||||
def extract_cert_info(self, cert_file):
|
||||
# Существующая логика парсинга через openssl
|
||||
try:
|
||||
result = subprocess.run(['openssl', 'x509', '-in', cert_file, '-noout', '-text'],
|
||||
capture_output=True, text=True, check=True)
|
||||
output = result.stdout
|
||||
data = {'file': os.path.basename(cert_file), 'file_path': cert_file, 'subject': 'N/A',
|
||||
'issuer': 'N/A', 'not_after': 'N/A'}
|
||||
'issuer': 'N/A', 'not_after': 'N/A', 'not_before': 'N/A', 'serial': 'N/A', 'type': 'Unknown'}
|
||||
|
||||
is_ca = False
|
||||
extended_usage = ""
|
||||
|
||||
for line in output.split('\n'):
|
||||
line = line.strip()
|
||||
if line.startswith('Subject:'):
|
||||
data['subject'] = line.split('Subject:', 1)[1].strip()
|
||||
cn_match = re.search(r'CN=([^,]+)', data['subject'])
|
||||
if cn_match: data['common_name'] = cn_match.group(1)
|
||||
cn_match = re.search(r'CN\s*=\s*([^,]+)', data['subject'])
|
||||
if cn_match: data['common_name'] = cn_match.group(1).strip()
|
||||
elif 'Not After' in line:
|
||||
data['not_after'] = line.split(':', 1)[1].strip()
|
||||
|
||||
elif 'Not Before' in line:
|
||||
data['not_before'] = line.split(':', 1)[1].strip()
|
||||
elif 'Serial Number:' in line:
|
||||
data['serial'] = line.split(':', 1)[1].strip()
|
||||
elif 'CA:TRUE' in line:
|
||||
is_ca = True
|
||||
elif 'TLS Web Server Authentication' in line:
|
||||
extended_usage += "Server "
|
||||
elif 'TLS Web Client Authentication' in line:
|
||||
extended_usage += "Client "
|
||||
|
||||
# Determine Type
|
||||
if is_ca:
|
||||
data['type'] = 'CA'
|
||||
elif 'Server' in extended_usage:
|
||||
data['type'] = 'Server'
|
||||
elif 'Client' in extended_usage:
|
||||
data['type'] = 'Client'
|
||||
elif 'server' in data.get('common_name', '').lower():
|
||||
data['type'] = 'Server'
|
||||
else:
|
||||
data['type'] = 'Client' # Default to client if ambiguous
|
||||
|
||||
if data['not_after'] != 'N/A':
|
||||
data['sort_date'] = self.parse_openssl_date(data['not_after']).isoformat()
|
||||
else:
|
||||
data['sort_date'] = datetime.min.isoformat()
|
||||
|
||||
# Parse dates for UI
|
||||
if data['not_after'] != 'N/A':
|
||||
dt = self.parse_openssl_date(data['not_after'])
|
||||
data['expires_iso'] = dt.isoformat()
|
||||
|
||||
if data['not_before'] != 'N/A':
|
||||
dt = self.parse_openssl_date(data['not_before'])
|
||||
data['issued_iso'] = dt.isoformat()
|
||||
|
||||
data['days_remaining'] = self.calculate_days_remaining(data['not_after'])
|
||||
data['is_expired'] = 'Expired' in data['days_remaining']
|
||||
|
||||
# State for UI
|
||||
if data['is_expired']:
|
||||
data['state'] = 'Expired'
|
||||
else:
|
||||
data['state'] = 'Valid'
|
||||
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing {cert_file}: {e}")
|
||||
@@ -559,6 +617,46 @@ class OpenVPNAPI:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_active_sessions(self):
|
||||
"""Get list of currently active sessions from temporary table"""
|
||||
conn = self.get_db_connection()
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
# Check if table exists first (graceful degradation)
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='active_sessions'")
|
||||
if not cursor.fetchone():
|
||||
return []
|
||||
|
||||
cursor.execute('''
|
||||
SELECT
|
||||
client_id, common_name, real_address, bytes_received, bytes_sent, connected_since, last_seen
|
||||
FROM active_sessions
|
||||
ORDER BY connected_since DESC
|
||||
''')
|
||||
|
||||
rows = cursor.fetchall()
|
||||
result = []
|
||||
for row in rows:
|
||||
result.append({
|
||||
'client_id': row['client_id'],
|
||||
'common_name': row['common_name'],
|
||||
'real_address': row['real_address'],
|
||||
'bytes_received': row['bytes_received'],
|
||||
'bytes_sent': row['bytes_sent'],
|
||||
'connected_since': row['connected_since'],
|
||||
'last_seen': row['last_seen'],
|
||||
# Calculated fields for convenience
|
||||
'received_mb': round((row['bytes_received'] or 0) / (1024*1024), 2),
|
||||
'sent_mb': round((row['bytes_sent'] or 0) / (1024*1024), 2)
|
||||
})
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching active sessions: {e}")
|
||||
return []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# Initialize API instance
|
||||
api = OpenVPNAPI()
|
||||
|
||||
@@ -717,6 +815,339 @@ def get_analytics():
|
||||
logger.error(f"Error in analytics endpoint: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/v1/sessions', methods=['GET'])
|
||||
def get_sessions():
|
||||
"""Get all currently active sessions (real-time)"""
|
||||
try:
|
||||
data = api.get_active_sessions()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'timestamp': datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'data': data,
|
||||
'count': len(data)
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# --- PKI MANAGEMENT ROUTES ---
|
||||
|
||||
@app.route('/api/v1/pki/init', methods=['POST'])
|
||||
def init_pki():
|
||||
"""Initialize PKI environment"""
|
||||
try:
|
||||
force = request.json.get('force', False)
|
||||
pki_vars = request.json.get('vars', {})
|
||||
|
||||
# 0. Update Vars if provided
|
||||
if pki_vars:
|
||||
api.pki.update_vars(pki_vars)
|
||||
|
||||
# 1. Clean/Init PKI
|
||||
success, msg = api.pki.init_pki(force=force)
|
||||
if not success: return jsonify({'success': False, 'error': msg}), 400
|
||||
|
||||
# 2. Build CA
|
||||
# Use CN from vars if available, else default
|
||||
ca_cn = pki_vars.get('EASYRSA_REQ_CN', 'OpenVPN-CA')
|
||||
api.pki.build_ca(ca_cn)
|
||||
|
||||
# 3. Build Server Cert
|
||||
api.pki.build_server("server")
|
||||
|
||||
# 4. Gen DH
|
||||
api.pki.gen_dh()
|
||||
|
||||
# 5. Gen TA Key
|
||||
# Ensure pki dir exists
|
||||
ta_path = Path(api.pki_path) / 'ta.key'
|
||||
api.pki.gen_ta_key(ta_path)
|
||||
|
||||
# 6. Gen CRL
|
||||
api.pki.gen_crl()
|
||||
|
||||
return jsonify({'success': True, 'message': 'PKI initialized successfully'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/v1/pki/validate', methods=['POST'])
|
||||
def validate_pki():
|
||||
"""Validate PKI path"""
|
||||
try:
|
||||
path = request.json.get('path')
|
||||
if not path: return jsonify({'success': False, 'error': 'Path required'}), 400
|
||||
success, msg = api.pki.validate_pki_path(path)
|
||||
return jsonify({'success': success, 'message': msg})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/v1/pki/config', methods=['GET', 'POST'])
|
||||
def handle_pki_config():
|
||||
"""Get or Save PKI path configuration"""
|
||||
try:
|
||||
if request.method == 'GET':
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'easyrsa_path': api.easyrsa_path,
|
||||
'pki_path': api.pki_path
|
||||
}
|
||||
})
|
||||
|
||||
# POST
|
||||
path_str = request.json.get('path')
|
||||
if not path_str: return jsonify({'success': False, 'error': 'Path required'}), 400
|
||||
|
||||
path = Path(path_str).resolve()
|
||||
if not path.exists(): return jsonify({'success': False, 'error': 'Path invalid'}), 400
|
||||
|
||||
# Heuristic to determine easyrsa_path and pki_path
|
||||
# User supplied 'path' is likely the PKI directory (containing ca.crt or being empty/prepared)
|
||||
pki_path = path
|
||||
easyrsa_path = path.parent # Default assumption: script is in parent
|
||||
|
||||
# 1. Search for easyrsa binary (Heuristic)
|
||||
potential_bins = [
|
||||
path / 'easyrsa', # Inside path
|
||||
path.parent / 'easyrsa', # Parent
|
||||
path.parent / 'easy-rsa' / 'easyrsa', # Sibling easy-rsa
|
||||
Path('/usr/share/easy-rsa/easyrsa'), # System
|
||||
Path('/etc/openvpn/easy-rsa/easyrsa') # System
|
||||
]
|
||||
|
||||
found_bin = None
|
||||
for bin_path in potential_bins:
|
||||
if bin_path.exists():
|
||||
easyrsa_path = bin_path.parent
|
||||
found_bin = bin_path
|
||||
break
|
||||
|
||||
# Override with explicit easyrsa_path if provided
|
||||
explicit_easyrsa = request.json.get('easyrsa_path')
|
||||
if explicit_easyrsa:
|
||||
epath = Path(explicit_easyrsa)
|
||||
if epath.is_file(): # Path to script
|
||||
easyrsa_path = epath.parent
|
||||
found_bin = epath
|
||||
elif (epath / 'easyrsa').exists(): # Path to dir
|
||||
easyrsa_path = epath
|
||||
found_bin = epath / 'easyrsa'
|
||||
|
||||
if not found_bin:
|
||||
# Fallback: assume typical layout if not found yet
|
||||
pass
|
||||
|
||||
# If user pointed to root (containing pki subdir)
|
||||
if (path / 'pki' / 'ca.crt').exists() or ((path / 'pki').exists() and not (path / 'ca.crt').exists()):
|
||||
pki_path = path / 'pki'
|
||||
# Only adjust easyrsa_path if not explicitly set/found yet
|
||||
if not explicit_easyrsa and not found_bin and (path / 'easyrsa').exists():
|
||||
easyrsa_path = path
|
||||
|
||||
# Update Config
|
||||
if not api.config.has_section('pki'):
|
||||
api.config.add_section('pki')
|
||||
|
||||
api.config.set('pki', 'easyrsa_path', str(easyrsa_path))
|
||||
api.config.set('pki', 'pki_path', str(pki_path))
|
||||
|
||||
# Write config.ini
|
||||
with open('config.ini', 'w') as f:
|
||||
api.config.write(f)
|
||||
|
||||
# Reload PKI Manager
|
||||
api.easyrsa_path = str(easyrsa_path)
|
||||
api.pki_path = str(pki_path)
|
||||
api.pki = PKIManager(api.easyrsa_path, api.pki_path)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'PKI Conf saved',
|
||||
'details': {
|
||||
'easyrsa_path': str(easyrsa_path),
|
||||
'pki_path': str(pki_path)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/v1/pki/client/<string:name>/config', methods=['GET'])
|
||||
def get_client_config(name):
|
||||
"""Get client config (generate on fly)"""
|
||||
try:
|
||||
# Get defaults from active server config
|
||||
server_conf = api.conf_mgr.read_server_config()
|
||||
|
||||
# Determine public host
|
||||
host = request.args.get('server_ip')
|
||||
if not host:
|
||||
host = server_conf.get('public_ip')
|
||||
if not host:
|
||||
try:
|
||||
import socket
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
host = s.getsockname()[0]
|
||||
s.close()
|
||||
except:
|
||||
host = '127.0.0.1'
|
||||
|
||||
extra_params = {
|
||||
'remote_host': host,
|
||||
'remote_port': request.args.get('port') or server_conf.get('port', 1194),
|
||||
'proto': request.args.get('proto') or server_conf.get('proto', 'udp')
|
||||
}
|
||||
|
||||
succ_conf, conf_content = api.conf_mgr.generate_client_config(
|
||||
name, api.pki_path, server_conf, extra_params
|
||||
)
|
||||
|
||||
if not succ_conf: return jsonify({'success': False, 'error': conf_content}), 500
|
||||
|
||||
return jsonify({'success': True, 'config': conf_content, 'filename': f"{name}.ovpn"})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/v1/pki/client', methods=['POST'])
|
||||
def create_client():
|
||||
"""Create new client and return config"""
|
||||
try:
|
||||
data = request.json
|
||||
name = data.get('name')
|
||||
if not name: return jsonify({'success': False, 'error': 'Name is required'}), 400
|
||||
|
||||
# 1. Build Cert
|
||||
success, output = api.pki.build_client(name)
|
||||
if not success: return jsonify({'success': False, 'error': output}), 500
|
||||
|
||||
# 2. Generate Config (Just to verify it works, but we don't strictly need to return it if UI doesn't download it automatically.
|
||||
# However, it's good practice to return it in creation response too, in case UI changes mind)
|
||||
server_ip = data.get('server_ip') or api.public_ip or '127.0.0.1'
|
||||
|
||||
# Get defaults from active server config
|
||||
server_conf = api.conf_mgr.read_server_config()
|
||||
def_port = server_conf.get('port', 1194)
|
||||
def_proto = server_conf.get('proto', 'udp')
|
||||
|
||||
succ_conf, conf_content = api.conf_mgr.generate_client_config(
|
||||
name, api.pki_path, server_ip, data.get('port', def_port), data.get('proto', def_proto)
|
||||
)
|
||||
|
||||
if not succ_conf: return jsonify({'success': False, 'error': conf_content}), 500
|
||||
|
||||
return jsonify({'success': True, 'config': conf_content, 'filename': f"{name}.ovpn"})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/v1/pki/client/<string:name>', methods=['DELETE'])
|
||||
def revoke_client(name):
|
||||
"""Revoke client certificate"""
|
||||
try:
|
||||
success, output = api.pki.revoke_client(name)
|
||||
if not success: return jsonify({'success': False, 'error': output}), 500
|
||||
return jsonify({'success': True, 'message': 'Client revoked'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
# --- SERVER MANAGEMENT ROUTES ---
|
||||
|
||||
@app.route('/api/v1/server/config', methods=['GET', 'POST'])
|
||||
def manage_server_config():
|
||||
"""Get or Save server.conf"""
|
||||
try:
|
||||
if request.method == 'GET':
|
||||
# Check for path override (Reload from specific file)
|
||||
path_arg = request.args.get('path')
|
||||
|
||||
if path_arg:
|
||||
# Update path preference if requested
|
||||
new_path_str = str(path_arg)
|
||||
if new_path_str != str(api.conf_mgr.server_conf_path):
|
||||
api.server_config_path = new_path_str
|
||||
api.conf_mgr.server_conf_path = Path(new_path_str)
|
||||
|
||||
if not api.config.has_section('server'): api.config.add_section('server')
|
||||
api.config.set('server', 'config_path', new_path_str)
|
||||
with open('config.ini', 'w') as f:
|
||||
api.config.write(f)
|
||||
|
||||
current_conf = api.conf_mgr.read_server_config()
|
||||
# Enriched with meta-config
|
||||
current_conf['config_path'] = str(api.conf_mgr.server_conf_path)
|
||||
current_conf['public_ip'] = api.public_ip
|
||||
return jsonify({'success': True, 'data': current_conf})
|
||||
|
||||
# POST
|
||||
params = request.json
|
||||
# Basic validation
|
||||
if not params.get('port'): return jsonify({'success': False, 'error': 'Port required'}), 400
|
||||
|
||||
# Check/Update Config Path and Public IP
|
||||
new_path = params.get('config_path')
|
||||
new_ip = params.get('public_ip')
|
||||
|
||||
config_updated = False
|
||||
if new_path and str(new_path) != str(api.conf_mgr.server_conf_path):
|
||||
api.server_config_path = str(new_path)
|
||||
api.conf_mgr.server_conf_path = Path(new_path)
|
||||
if not api.config.has_section('server'): api.config.add_section('server')
|
||||
api.config.set('server', 'config_path', str(new_path))
|
||||
config_updated = True
|
||||
|
||||
if new_ip is not None and new_ip != api.public_ip: # Allow empty string
|
||||
api.public_ip = new_ip
|
||||
if not api.config.has_section('openvpn_monitor'): api.config.add_section('openvpn_monitor')
|
||||
api.config.set('openvpn_monitor', 'public_ip', new_ip)
|
||||
config_updated = True
|
||||
|
||||
if config_updated:
|
||||
with open('config.ini', 'w') as f:
|
||||
api.config.write(f)
|
||||
|
||||
# Define paths
|
||||
params['ca_path'] = str(Path(api.pki_path) / 'ca.crt')
|
||||
params['cert_path'] = str(Path(api.pki_path) / 'issued/server.crt')
|
||||
params['key_path'] = str(Path(api.pki_path) / 'private/server.key')
|
||||
params['dh_path'] = str(Path(api.pki_path) / 'dh.pem')
|
||||
params['ta_path'] = str(Path(api.pki_path) / 'ta.key')
|
||||
params['crl_path'] = str(Path(api.pki_path) / 'crl.pem')
|
||||
|
||||
success, msg = api.conf_mgr.generate_server_config(params)
|
||||
if not success: return jsonify({'success': False, 'error': msg}), 500
|
||||
|
||||
return jsonify({'success': True, 'path': msg})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/v1/server/action', methods=['POST'])
|
||||
def server_action():
|
||||
"""Start/Stop/Restart OpenVPN service"""
|
||||
try:
|
||||
action = request.json.get('action')
|
||||
if action == 'start':
|
||||
success, msg = api.service.start()
|
||||
elif action == 'stop':
|
||||
success, msg = api.service.stop()
|
||||
elif action == 'restart':
|
||||
success, msg = api.service.restart()
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'Invalid action'}), 400
|
||||
|
||||
if not success: return jsonify({'success': False, 'error': msg}), 500
|
||||
return jsonify({'success': True, 'message': msg})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/v1/server/status', methods=['GET'])
|
||||
def server_status():
|
||||
"""Get service status"""
|
||||
try:
|
||||
status = api.service.get_status()
|
||||
return jsonify({'success': True, 'status': status})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
if __name__ == "__main__":
|
||||
host = api.config.get('api', 'host', fallback='0.0.0.0')
|
||||
port = 5001 # Используем 5001, чтобы не конфликтовать, если что-то уже есть на 5000
|
||||
|
||||
Reference in New Issue
Block a user