docker environment control improvement
This commit is contained in:
@@ -4,10 +4,7 @@ from datetime import datetime, timedelta, timezone
|
||||
from flask import Flask, jsonify, request, send_file
|
||||
from flask_cors import CORS
|
||||
import logging
|
||||
import subprocess
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import jwt
|
||||
import pyotp
|
||||
import bcrypt
|
||||
@@ -31,6 +28,17 @@ app = Flask(__name__)
|
||||
CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True)
|
||||
|
||||
class OpenVPNAPI:
|
||||
def get_config_value(self, section, key, fallback=None):
|
||||
try:
|
||||
# Priority: ENV > Config File > Fallback
|
||||
env_key = f"OVPMON_{section.upper()}_{key.upper()}".replace('-', '_').replace(' ', '_')
|
||||
env_val = os.getenv(env_key)
|
||||
if env_val is not None:
|
||||
return env_val
|
||||
return self.config.get(section, key, fallback=fallback)
|
||||
except:
|
||||
return fallback
|
||||
|
||||
def __init__(self, config_file='config.ini'):
|
||||
self.db_manager = DatabaseManager(config_file)
|
||||
self.db_manager.init_database()
|
||||
@@ -38,21 +46,10 @@ class OpenVPNAPI:
|
||||
self.config.read(config_file)
|
||||
|
||||
# Paths
|
||||
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 = {}
|
||||
self.public_ip = self.get_config_value('openvpn_monitor', 'public_ip', fallback='')
|
||||
|
||||
# Security
|
||||
# Priority 1: Environment Variable
|
||||
# Priority 2: Config file
|
||||
self.secret_key = os.getenv('OVPMON_SECRET_KEY') or self.config.get('api', 'secret_key', fallback='ovpmon-secret-change-me')
|
||||
self.secret_key = self.get_config_value('api', 'secret_key', fallback='ovpmon-secret-change-me')
|
||||
app.config['SECRET_KEY'] = self.secret_key
|
||||
|
||||
# Ensure at least one user exists
|
||||
@@ -130,140 +127,7 @@ class OpenVPNAPI:
|
||||
conn.close()
|
||||
|
||||
# --- БЛОК РАБОТЫ С СЕРТИФИКАТАМИ (Оставлен без изменений) ---
|
||||
def parse_openssl_date(self, date_str):
|
||||
try:
|
||||
parts = date_str.split()
|
||||
if len(parts[1]) == 1:
|
||||
parts[1] = f' {parts[1]}'
|
||||
normalized_date = ' '.join(parts)
|
||||
return datetime.strptime(normalized_date, '%b %d %H:%M:%S %Y GMT')
|
||||
except ValueError:
|
||||
try:
|
||||
return datetime.strptime(date_str, '%b %d %H:%M:%S %Y %Z')
|
||||
except ValueError:
|
||||
logger.warning(f"Could not parse date: {date_str}")
|
||||
return datetime.min
|
||||
|
||||
def calculate_days_remaining(self, not_after_str):
|
||||
if not_after_str == 'N/A': return 'N/A'
|
||||
try:
|
||||
expiration_date = self.parse_openssl_date(not_after_str)
|
||||
if expiration_date == datetime.min: return 'N/A'
|
||||
days_remaining = (expiration_date - datetime.now()).days
|
||||
if days_remaining < 0: return f"Expired ({abs(days_remaining)} days ago)"
|
||||
else: return f"{days_remaining} days"
|
||||
except Exception: return 'N/A'
|
||||
|
||||
def extract_cert_info(self, cert_file):
|
||||
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', '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\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}")
|
||||
return None
|
||||
|
||||
def get_certificates_info(self):
|
||||
cert_path = Path(self.certificates_path)
|
||||
if not cert_path.exists(): return []
|
||||
|
||||
cert_files = []
|
||||
for ext in self.cert_extensions:
|
||||
cert_files.extend(cert_path.rglob(f'*.{ext.strip()}'))
|
||||
|
||||
current_valid_files = set()
|
||||
cert_data = []
|
||||
|
||||
for cert_file_path in cert_files:
|
||||
cert_file = str(cert_file_path)
|
||||
current_valid_files.add(cert_file)
|
||||
|
||||
try:
|
||||
mtime = os.path.getmtime(cert_file)
|
||||
|
||||
# Check cache
|
||||
cached = self._cert_cache.get(cert_file)
|
||||
if cached and cached['mtime'] == mtime:
|
||||
cert_data.append(cached['data'])
|
||||
else:
|
||||
# Parse and update cache
|
||||
parsed_data = self.extract_cert_info(cert_file)
|
||||
if parsed_data:
|
||||
self._cert_cache[cert_file] = {
|
||||
'mtime': mtime,
|
||||
'data': parsed_data
|
||||
}
|
||||
cert_data.append(parsed_data)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
# Prune cache for deleted files
|
||||
for cached_file in list(self._cert_cache.keys()):
|
||||
if cached_file not in current_valid_files:
|
||||
del self._cert_cache[cached_file]
|
||||
|
||||
return cert_data
|
||||
# -----------------------------------------------------------
|
||||
# -----------------------------------------------------------
|
||||
|
||||
def get_current_stats(self):
|
||||
@@ -1109,14 +973,7 @@ def get_client_stats(common_name):
|
||||
logger.error(f"API Error: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/v1/certificates', methods=['GET'])
|
||||
@token_required
|
||||
def get_certificates():
|
||||
try:
|
||||
data = api.get_certificates_info()
|
||||
return jsonify({'success': True, 'data': data})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/v1/clients', methods=['GET'])
|
||||
@token_required
|
||||
@@ -1180,9 +1037,9 @@ def get_sessions():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
host = api.config.get('api', 'host', fallback='0.0.0.0')
|
||||
port = 5001 # Используем 5001, чтобы не конфликтовать, если что-то уже есть на 5000
|
||||
debug = api.config.getboolean('api', 'debug', fallback=False)
|
||||
host = api.get_config_value('api', 'host', fallback='0.0.0.0')
|
||||
port = int(api.get_config_value('api', 'port', fallback=5001))
|
||||
debug = api.get_config_value('api', 'debug', fallback='false').lower() == 'true'
|
||||
|
||||
logger.info(f"Starting API on {host}:{port}")
|
||||
app.run(host=host, port=port, debug=debug)
|
||||
Reference in New Issue
Block a user