diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index dab9a4e17afd2ef39d90ccb0b40ef2786fe77422..0000000000000000000000000000000000000000 --- a/.gitattributes +++ /dev/null @@ -1,35 +0,0 @@ -*.7z filter=lfs diff=lfs merge=lfs -text -*.arrow filter=lfs diff=lfs merge=lfs -text -*.bin filter=lfs diff=lfs merge=lfs -text -*.bz2 filter=lfs diff=lfs merge=lfs -text -*.ckpt filter=lfs diff=lfs merge=lfs -text -*.ftz filter=lfs diff=lfs merge=lfs -text -*.gz filter=lfs diff=lfs merge=lfs -text -*.h5 filter=lfs diff=lfs merge=lfs -text -*.joblib filter=lfs diff=lfs merge=lfs -text -*.lfs.* filter=lfs diff=lfs merge=lfs -text -*.mlmodel filter=lfs diff=lfs merge=lfs -text -*.model filter=lfs diff=lfs merge=lfs -text -*.msgpack filter=lfs diff=lfs merge=lfs -text -*.npy filter=lfs diff=lfs merge=lfs -text -*.npz filter=lfs diff=lfs merge=lfs -text -*.onnx filter=lfs diff=lfs merge=lfs -text -*.ot filter=lfs diff=lfs merge=lfs -text -*.parquet filter=lfs diff=lfs merge=lfs -text -*.pb filter=lfs diff=lfs merge=lfs -text -*.pickle filter=lfs diff=lfs merge=lfs -text -*.pkl filter=lfs diff=lfs merge=lfs -text -*.pt filter=lfs diff=lfs merge=lfs -text -*.pth filter=lfs diff=lfs merge=lfs -text -*.rar filter=lfs diff=lfs merge=lfs -text -*.safetensors filter=lfs diff=lfs merge=lfs -text -saved_model/**/* filter=lfs diff=lfs merge=lfs -text -*.tar.* filter=lfs diff=lfs merge=lfs -text -*.tar filter=lfs diff=lfs merge=lfs -text -*.tflite filter=lfs diff=lfs merge=lfs -text -*.tgz filter=lfs diff=lfs merge=lfs -text -*.wasm filter=lfs diff=lfs merge=lfs -text -*.xz filter=lfs diff=lfs merge=lfs -text -*.zip filter=lfs diff=lfs merge=lfs -text -*.zst filter=lfs diff=lfs merge=lfs -text -*tfevents* filter=lfs diff=lfs merge=lfs -text diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index fc529740dd7e44223371a8813151ed5fdbb56768..0000000000000000000000000000000000000000 --- a/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -# VPN Server with FastAPI -FROM python:3.11-slim - -# Set working directory -WORKDIR /app - -# Install system dependencies -RUN apt-get update && apt-get install -y \ - build-essential \ - python3-dev \ - && rm -rf /var/lib/apt/lists/* - -# Copy requirements and install Python dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Create non-root user -RUN useradd -m -u 1000 vpnuser && \ - chown -R vpnuser:vpnuser /app - -# Copy application files -COPY --chown=vpnuser:vpnuser . . - -# Set environment variables -ENV PYTHONPATH=/app -ENV PYTHONUNBUFFERED=1 - -# Switch to non-root user -USER vpnuser - -# Expose port -EXPOSE 7860 - -# Run the application with uvicorn -CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "4"] diff --git a/admin_routes.py b/admin_routes.py deleted file mode 100644 index 429e90f1e418f91fcd66240e86af1ccae2bab606..0000000000000000000000000000000000000000 --- a/admin_routes.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -Admin routes and functionality for Outline VPN -""" -import os -import json -import psutil -import zipfile -from datetime import datetime -from flask import jsonify, request, send_file, flash, redirect, url_for -from flask_login import login_required, current_user -from . import app -from .models import User, UserRole, SystemHealth, AuditLog, Alert -from .services import backup_service, monitoring_service - -def admin_required(f): - """Decorator to require admin role for routes""" - @wraps(f) - def decorated_function(*args, **kwargs): - if not current_user.is_authenticated or current_user.role != UserRole.ADMIN: - flash('You need administrator privileges to access this page.') - return redirect(url_for('dashboard')) - return f(*args, **kwargs) - return decorated_function - -@app.route('/admin') -@login_required -@admin_required -def admin_dashboard(): - """Admin dashboard view""" - system_health = monitoring_service.get_system_health() - active_alerts = Alert.query.filter_by(status='active').order_by(Alert.created_at.desc()).all() - audit_logs = AuditLog.query.order_by(AuditLog.timestamp.desc()).limit(50).all() - - return render_template('admin.html', - system_health=system_health, - active_alerts=active_alerts, - audit_logs=audit_logs) - -@app.route('/api/system-health') -@login_required -@admin_required -def get_system_health(): - """Get real-time system health metrics""" - return jsonify(monitoring_service.get_system_health()) - -@app.route('/api/update-server-config', methods=['POST']) -@login_required -@admin_required -def update_server_config(): - """Update server configuration""" - try: - config = request.get_json() - backup_service.backup_config('pre_update') # Create backup before updating - - # Update configuration - current_config = ServerConfig.query.first() - for key, value in config.items(): - setattr(current_config, key, value) - - db.session.commit() - - # Log the change - AuditLog.create( - user_id=current_user.id, - action='update_config', - details='Server configuration updated' - ) - - # Restart required services - monitoring_service.restart_services() - - return jsonify({'status': 'success'}) - except Exception as e: - return jsonify({'status': 'error', 'message': str(e)}), 500 - -@app.route('/api/create-backup') -@login_required -@admin_required -def create_backup(): - """Create a backup of server configuration""" - try: - include_user_data = request.args.get('include_user_data', 'false') == 'true' - backup_path = backup_service.create_backup(include_user_data) - - # Log the backup creation - AuditLog.create( - user_id=current_user.id, - action='create_backup', - details=f'Backup created: {os.path.basename(backup_path)}' - ) - - return send_file( - backup_path, - as_attachment=True, - download_name=f'outline_backup_{datetime.now().strftime("%Y%m%d_%H%M%S")}.zip' - ) - except Exception as e: - flash(f'Error creating backup: {str(e)}', 'error') - return redirect(url_for('admin_dashboard')) - -@app.route('/api/restore-config', methods=['POST']) -@login_required -@admin_required -def restore_config(): - """Restore server configuration from backup""" - try: - if 'backup_file' not in request.files: - flash('No backup file provided', 'error') - return redirect(url_for('admin_dashboard')) - - backup_file = request.files['backup_file'] - if backup_file.filename == '': - flash('No backup file selected', 'error') - return redirect(url_for('admin_dashboard')) - - # Create backup of current configuration - backup_service.backup_config('pre_restore') - - # Restore from backup - backup_service.restore_from_backup(backup_file) - - # Log the restore - AuditLog.create( - user_id=current_user.id, - action='restore_config', - details=f'Configuration restored from {backup_file.filename}' - ) - - flash('Configuration restored successfully', 'success') - return redirect(url_for('admin_dashboard')) - except Exception as e: - flash(f'Error restoring configuration: {str(e)}', 'error') - return redirect(url_for('admin_dashboard')) - -@app.route('/api/export-audit-log') -@login_required -@admin_required -def export_audit_log(): - """Export audit log in specified format""" - format = request.args.get('format', 'csv') - logs = AuditLog.query.order_by(AuditLog.timestamp.desc()).all() - - if format == 'csv': - output = io.StringIO() - writer = csv.writer(output) - writer.writerow(['Timestamp', 'User', 'Action', 'Details']) - for log in logs: - writer.writerow([ - log.timestamp.strftime('%Y-%m-%d %H:%M:%S'), - log.user.username, - log.action, - log.details - ]) - - return Response( - output.getvalue(), - mimetype='text/csv', - headers={'Content-Disposition': 'attachment; filename=audit_log.csv'} - ) - elif format == 'json': - return jsonify([{ - 'timestamp': log.timestamp.strftime('%Y-%m-%d %H:%M:%S'), - 'user': log.user.username, - 'action': log.action, - 'details': log.details - } for log in logs]) diff --git a/app.py b/app.py deleted file mode 100644 index ea76206db6833899273e5b323de2de518d48b8ee..0000000000000000000000000000000000000000 --- a/app.py +++ /dev/null @@ -1,819 +0,0 @@ -""" -Main application entry point -""" -from fastapi import FastAPI, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates - -from routers import admin, users, vpn -from core.error_handlers import setup_error_handlers -from core.database import init_db -from core.logger import setup_logging -from core.middleware import RequestLoggerMiddleware, ErrorHandlerMiddleware - -import logging -import os -import requests -import socket -from starlette.responses import RedirectResponse as StarletteRedirect -from starlette.status import HTTP_302_FOUND, HTTP_303_SEE_OTHER -import logging -import json -import asyncio -import threading -import os -import json -import uuid -import bcrypt -from datetime import datetime, timedelta -import logging -from typing import Dict, Optional, List -from sqlalchemy.orm import Session -# Initialize logging -setup_logging() -logger = logging.getLogger(__name__) - -# Create FastAPI application -app = FastAPI( - title="VPN Server API", - description="API for managing VPN server and users", - version="1.0.0" -) - -# Configure CORS -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # Configure this properly in production - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Add custom middleware -app.add_middleware(RequestLoggerMiddleware) -app.add_middleware(ErrorHandlerMiddleware) - -# Configure static files and templates -app.mount("/static", StaticFiles(directory="static"), name="static") -templates = Jinja2Templates(directory="templates") - -# Include routers -app.include_router(admin.router, prefix="/api") -app.include_router(users.router, prefix="/api") -app.include_router(vpn.router, prefix="/api") - -# Setup error handlers -setup_error_handlers(app) - -@app.on_event("startup") -async def startup_event(): - """Initialize application on startup""" - try: - # Initialize database - await init_db() - logger.info("Database initialized successfully") - - except Exception as e: - logger.error(f"Failed to initialize application: {e}") - raise - -@app.get("/") -async def root(request: Request): - """Root endpoint - renders the main template""" - return templates.TemplateResponse( - "index.html", - {"request": request} - ) - -@app.get("/health") -async def health_check(): - """Health check endpoint""" - return {"status": "healthy"} -# Database dependency -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - -# OAuth2 password bearer for token auth -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) - -# Pydantic models for request/response validation -class Token(BaseModel): - access_token: str - token_type: str - -app = FastAPI() - -# Configure static files and templates -app.mount("/static", StaticFiles(directory="web/static"), name="static") -templates = Jinja2Templates(directory="web/templates") - -# Add template context processor for static URLs and other global helpers -def static_url(path: str) -> str: - return f"/static/{path}" - -@app.get("/api/user/current") -async def get_current_user(request: Request, db: Session = Depends(get_db)): - try: - user = await get_optional_user(request, db) - if user: - return { - "username": user.username, - "id": str(user.id), - "config_id": user.config_id - } - return None - except Exception: - return None - -templates.env.globals.update({ - "static_url": static_url, - "url_for": lambda name, **params: f"/{name}" if name != "static" else static_url(params.get("filename", "")), -}) - -# OAuth2 password bearer for token auth -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - -# Pydantic models for request/response validation -class Token(BaseModel): - access_token: str - token_type: str - -class TokenData(BaseModel): - username: Optional[str] = None - -class UserBase(BaseModel): - email: EmailStr - -class UserCreate(UserBase): - password: str - -class UserInDB(UserBase): - hashed_password: str - config_id: str - created_at: datetime - - class Config: - orm_mode = True - -async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") - if username is None: - raise credentials_exception - token_data = TokenData(username=username) - except JWTError: - raise credentials_exception - - user = db.query(User).filter(User.username == token_data.username).first() - if user is None: - raise credentials_exception - return user - -def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): - to_encode = data.copy() - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(minutes=15) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt - -# Global VPN server state -vpn_server: Optional[OutlineServer] = None -session_tracker: Optional[SessionTracker] = None -logger: Optional[LogManager] = None - -# Initialize database -init_db() - -CONFIG_DIR = 'config' -USERS_FILE = os.path.join(CONFIG_DIR, 'users.json') -os.makedirs(CONFIG_DIR, exist_ok=True) - -def load_users(): - if os.path.exists(USERS_FILE): - with open(USERS_FILE, 'r') as f: - return json.load(f) - return {} - -def save_users(users): - with open(USERS_FILE, 'w') as f: - json.dump(users, f) - -def get_server_ip(): - """Get the server's public IP address""" - try: - # First try to get public IP from external service - response = requests.get('https://api.ipify.org', timeout=5) - if response.status_code == 200: - return response.text.strip() - except: - pass - - try: - # Try another public IP service as backup - response = requests.get('https://ifconfig.me', timeout=5) - if response.status_code == 200: - return response.text.strip() - except: - pass - - # Fallback: Get local IP - try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(('8.8.8.8', 80)) - local_ip = s.getsockname()[0] - s.close() - return local_ip - except: - # Last resort fallback - return '127.0.0.1' - -def initialize_ikev2_server(): - """Initialize IKEv2 server""" - global ikev2_server - server_ip = get_server_ip() - ikev2_server = IKEv2Server(server_ip, logger) - logger.log(LogLevel.INFO, LogCategory.SYSTEM, "app", "IKEv2 server initialized") - -def initialize_vpn_server(): - """Initialize the VPN server components""" - global vpn_server, session_tracker, logger, ikev2_server - - # Initialize logger - logger = LogManager() - logger.log(LogLevel.INFO, LogCategory.SYSTEM, "app", "Initializing VPN server") - - # Initialize session tracker - session_tracker = SessionTracker() - - # Initialize IKEv2 server - initialize_ikev2_server() - - # Initialize VPN server - server_ip = get_server_ip() - vpn_server_config = { - "server": { - "host": server_ip, # Use automatically detected server IP - "port": 8388, # Default Shadowsocks port - "virtual_network": "10.7.0.0/24", # Virtual network for client IPs - "protocols": { - "shadowsocks": { - "enabled": True, - "port": 8388 - }, - "wireguard": { - "enabled": True, - "port": 51820 - }, - "openvpn": { - "enabled": True, - "port": 1194 - }, - "ikev2": { - "enabled": True, - "port": 500 - } - } - }, - "security": { - "cipher": "aes-256-gcm", - "auth": "sha256", - "enable_perfect_forward_secrecy": True - } - } - vpn_server = OutlineServer(vpn_server_config) - # Start the VPN server in a separate thread - def run_server(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete(vpn_server.start()) - loop.run_forever() - - server_thread = threading.Thread(target=run_server, daemon=True) - server_thread.start() - logger.log(LogLevel.INFO, LogCategory.SYSTEM, "app", f"VPN server initialized and started on {server_ip}") - -def load_users(): - if os.path.exists(USERS_FILE): - with open(USERS_FILE, 'r') as f: - return json.load(f) - return {} - -def save_users(users): - with open(USERS_FILE, 'w') as f: - json.dump(users, f) - -def login_required(func): - @wraps(func) - async def wrapper(*args, **kwargs): - token = kwargs.get('token') - if not token: - return StarletteRedirect('/login', status_code=HTTP_303_SEE_OTHER) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") - if username is None: - return StarletteRedirect('/login', status_code=HTTP_303_SEE_OTHER) - except JWTError: - return StarletteRedirect('/login', status_code=HTTP_303_SEE_OTHER) - return await func(*args, **kwargs) - return wrapper - -async def get_optional_user( - request: Request, - db: Session = Depends(get_db) -) -> Optional[User]: - try: - auth = request.headers.get("Authorization") - if not auth: - return None - scheme, _, token = auth.partition(" ") - if scheme.lower() != "bearer": - return None - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") - if username is None: - return None - user = db.query(User).filter(User.username == username).first() - return user - except JWTError: - return None - except Exception: - return None - -@app.get("/", response_class=HTMLResponse) -async def index(request: Request): - return templates.TemplateResponse("index.html", {"request": request}) - -@app.post("/token") -async def login( - request: Request, - form_data: OAuth2PasswordRequestForm = Depends(), - db: Session = Depends(get_db) -): - user = db.query(User).filter(User.username == form_data.username).first() - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - - # Verify password - if not verify_password(form_data.password, user.hashed_password): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - - # Check if account is locked - if user.status == UserStatus.LOCKED: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Account is locked. Please contact support." - ) - - # Create access token - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={"sub": user.username}, expires_delta=access_token_expires - ) - - # Record successful login - user_service = UserService(db) - user_service.create_session( - user=user, - ip_address=request.client.host, - device_info=request.headers.get("user-agent", "") - ) - user.record_login_attempt(success=True) - db.commit() - - return {"access_token": access_token, "token_type": "bearer"} - -@app.post("/signup", response_model=Token) -async def signup(user: UserCreate, db: Session = Depends(get_db)): - # Check if user exists - db_user = db.query(User).filter(User.username == user.email).first() - if db_user: - raise HTTPException(status_code=400, detail="Email already registered") - - # Create new user - config_id = str(uuid.uuid4()) - hashed_password = get_password_hash(user.password) - db_user = User( - username=user.email, - hashed_password=hashed_password, - config_id=config_id, - created_at=datetime.utcnow() - ) - db.add(db_user) - db.commit() - db.refresh(db_user) - - # Create VPN configuration - create_user_config(config_id) - - # Create access token - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - access_token = create_access_token( - data={"sub": user.email}, expires_delta=access_token_expires - ) - - return {"access_token": access_token, "token_type": "bearer"} - -@app.get("/signup", response_class=HTMLResponse) -async def signup_form(request: Request): - return templates.TemplateResponse("signup.html", {"request": request}) - -@app.get("/dashboard", response_class=HTMLResponse) -async def dashboard(request: Request, current_user: User = Depends(get_current_user)): - stats = get_user_stats(current_user.config_id) - return templates.TemplateResponse("dashboard.html", { - "request": request, - "stats": stats - }) - -@app.get('/download_config') -async def download_config(current_user: User = Depends(get_current_user)): - config_path = os.path.join(CONFIG_DIR, f"{current_user.config_id}.json") - - if not os.path.exists(config_path): - raise HTTPException( - status_code=404, - detail="Configuration not found" - ) - - with open(config_path, 'r') as f: - config = json.load(f) - - return JSONResponse(content=config) - -@app.get('/api/stats') -async def get_stats(current_user: User = Depends(get_current_user)): - return JSONResponse(content=get_user_stats(current_user.config_id)) - -def get_server_ip(): - """Get the server's public IP address""" - try: - # First try to get public IP from external service - response = requests.get('https://api.ipify.org') - if response.status_code == 200: - return response.text.strip() - except: - pass - - # Fallback: Get local IP - try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(('8.8.8.8', 80)) - local_ip = s.getsockname()[0] - s.close() - return local_ip - except: - return '127.0.0.1' # Last resort fallback - -def initialize_ikev2_server(): - """Initialize IKEv2 server""" - global ikev2_server - server_ip = get_server_ip() - ikev2_server = IKEv2Server(server_ip, logger) - logger.log(LogLevel.INFO, LogCategory.SYSTEM, "app", "IKEv2 server initialized") - -def generate_ikev2_certificate(config_id: str) -> Dict: - """Generate IKEv2 certificates for a user""" - username = f"user_{config_id[:8]}" - password = str(uuid.uuid4()) - psk = str(uuid.uuid4()) - - try: - cert_data = ikev2_server.add_user(config_id, username, password, psk) - logger.info(LogCategory.SYSTEM, "app", f"Generated IKEv2 certificates for user {config_id}") - return cert_data - except Exception as e: - logger.error(LogCategory.SYSTEM, "app", f"Failed to generate IKEv2 certificates: {e}") - return None - -def create_user_config(config_id): - """Create Outline VPN configuration for a new user""" - if not os.path.exists(CONFIG_DIR): - os.makedirs(CONFIG_DIR) - - server_ip = get_server_ip() - access_key = str(uuid.uuid4()) - - # Outline/Shadowsocks config - ss_config = { - 'id': config_id, - 'server': { - 'host': server_ip, - 'port': 8388 # Shadowsocks port - }, - 'access_key': access_key, - 'protocol': 'shadowsocks', - 'created_at': datetime.now().isoformat() - } - - # IKEv2 config (Windows 10/11, Android 10+) - ikev2_config = { - 'id': f"{config_id}_ikev2", - 'server': { - 'host': server_ip, - 'port': 500 # IKEv2 port - }, - 'credentials': { - 'username': f"user_{config_id[:8]}", - 'password': str(uuid.uuid4()), - }, - 'psk': str(uuid.uuid4()), # Pre-shared key - 'certificate': generate_ikev2_certificate(config_id), - 'protocol': 'ikev2', - 'created_at': datetime.now().isoformat() - } - - # L2TP/IPsec config (Windows, Android) - l2tp_config = { - 'id': f"{config_id}_l2tp", - 'server': { - 'host': server_ip, - 'ports': { - 'l2tp': 1701, - 'ipsec': [500, 4500] # IPsec ports for NAT traversal - } - }, - 'credentials': { - 'username': f"user_{config_id[:8]}", - 'password': str(uuid.uuid4()) - }, - 'ipsec': { - 'psk': str(uuid.uuid4()), # Pre-shared key for IPsec - 'encryption': 'aes-256-cbc', - 'hash': 'sha256' - }, - 'protocol': 'l2tp_ipsec', - 'created_at': datetime.now().isoformat() - } - - # PPTP config (Legacy support - Windows, Android) - pptp_config = { - 'id': f"{config_id}_pptp", - 'server': { - 'host': server_ip, - 'port': 1723 # PPTP port - }, - 'credentials': { - 'username': f"user_{config_id[:8]}", - 'password': str(uuid.uuid4()) - }, - 'protocol': 'pptp', - 'encryption': 'require-mppe', # Maximum PPTP security - 'warning': 'PPTP is considered less secure, use IKEv2 or L2TP/IPsec when possible', - 'created_at': datetime.now().isoformat() - } - - # OpenVPN config (Universal support) - openvpn_config = { - 'id': f"{config_id}_openvpn", - 'server': { - 'host': server_ip, - 'port': 1194, # OpenVPN default port - 'protocol': 'udp' # UDP for better performance - }, - 'credentials': { - 'username': f"user_{config_id[:8]}", - 'password': str(uuid.uuid4()) - }, - 'certificates': generate_openvpn_certificates(config_id), - 'protocol': 'openvpn', - 'created_at': datetime.now().isoformat(), - 'config_file': generate_openvpn_config(config_id, server_ip) - } - - # WireGuard config (Built-in Windows 11, Android, iOS) - wireguard_config = { - 'id': f"{config_id}_wireguard", - 'server': { - 'host': server_ip, - 'port': 51820, # WireGuard default port - 'public_key': generate_wireguard_keys(config_id)['server_public'], - 'allowed_ips': ['0.0.0.0/0', '::/0'] # Route all traffic - }, - 'client': { - 'private_key': generate_wireguard_keys(config_id)['client_private'], - 'public_key': generate_wireguard_keys(config_id)['client_public'], - 'address': f'10.7.0.{2 + len(load_users())}', # Unique IP for each client - 'dns': ['1.1.1.1', '8.8.8.8'] - }, - 'protocol': 'wireguard', - 'created_at': datetime.now().isoformat() - } - - # L2TP/IPsec config (Built-in Windows, Android, iOS) - l2tp_config = { - 'id': f"{config_id}_l2tp", - 'server': { - 'host': server_ip, - 'port': 1701, # L2TP port - }, - 'credentials': { - 'username': f"user_{config_id[:8]}", - 'password': str(uuid.uuid4()) - }, - 'ipsec': { - 'psk': str(uuid.uuid4()) # Pre-shared key for IPsec - }, - 'protocol': 'l2tp_ipsec', - 'created_at': datetime.now().isoformat() - } - - # Combined config with all supported protocols - config = { - 'id': config_id, - 'protocols': { - 'shadowsocks': ss_config, - 'ikev2': ikev2_config, - 'l2tp': l2tp_config, - 'pptp': pptp_config - }, - 'recommended_protocol': { - 'windows': 'ikev2', - 'android': 'ikev2', - 'fallback': 'l2tp' - }, - 'created_at': datetime.now().isoformat() - } - - config_path = os.path.join(CONFIG_DIR, f"{config_id}.json") - with open(config_path, 'w') as f: - json.dump(config, f) - -def get_user_stats(config_id): - """Get real VPN usage statistics for a user from all active sessions""" - try: - if not session_tracker: - logger.error(LogCategory.SYSTEM, "app", "Session tracker not initialized") - return None - - # Get all sessions for this user - user_sessions = session_tracker.get_user_sessions(config_id) - if not user_sessions: - return { - 'bytes_sent': 0, - 'bytes_received': 0, - 'connected_since': None, - 'last_seen': None, - 'status': 'disconnected', - 'active_sessions': [], - 'protocols': [] - } - - # Aggregate stats from all active sessions - total_bytes_sent = 0 - total_bytes_received = 0 - earliest_connection = None - latest_seen = None - active_sessions = [] - used_protocols = set() - - for sess in user_sessions: - # Update totals - total_bytes_sent += sess.bytes_out - total_bytes_received += sess.bytes_in - - # Track connection times - session_start = datetime.fromtimestamp(sess.start_time) - session_last_seen = datetime.fromtimestamp(sess.last_seen) - - if not earliest_connection or session_start < earliest_connection: - earliest_connection = session_start - if not latest_seen or session_last_seen > latest_seen: - latest_seen = session_last_seen - - # Track protocols - used_protocols.add(sess.protocol) - - # Get session details - session_info = { - 'id': sess.session_id, - 'protocol': sess.protocol, - 'assigned_ip': sess.assigned_ip, - 'connected_since': session_start.isoformat(), - 'last_seen': session_last_seen.isoformat(), - 'bytes_sent': sess.bytes_out, - 'bytes_received': sess.bytes_in, - 'is_offline': sess.is_offline - } - active_sessions.append(session_info) - - # Determine overall status - current_time = datetime.now() - is_active = any( - (current_time - datetime.fromtimestamp(s.last_seen)).total_seconds() < 300 # 5 minutes - for s in user_sessions - ) - - status = 'active' if is_active else 'offline' - if not is_active and any(s.is_offline for s in user_sessions): - status = 'offline_available' - - return { - 'bytes_sent': total_bytes_sent, - 'bytes_received': total_bytes_received, - 'connected_since': earliest_connection.isoformat() if earliest_connection else None, - 'last_seen': latest_seen.isoformat() if latest_seen else None, - 'status': status, - 'active_sessions': active_sessions, - 'protocols': list(used_protocols) - } - - except Exception as e: - logger.error(LogCategory.SYSTEM, "app", f"Error getting user stats: {e}") - return None - -@app.post('/logout') -async def logout(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): - try: - # Find and end the current session - current_session = ( - db.query(UserSession) - .filter(UserSession.user_id == current_user.id) - .order_by(UserSession.created_at.desc()) - .first() - ) - if current_session: - current_session.expires_at = datetime.utcnow() - db.commit() - - return StarletteRedirect('/', status_code=HTTP_303_SEE_OTHER) - except Exception as e: - raise HTTPException( - status_code=500, - detail="Error during logout" - ) - -@app.get('/forgot-password') -async def forgot_password_form(request: Request): - return templates.TemplateResponse("forgot_password.html", {"request": request}) - -@app.post('/forgot-password') -async def forgot_password(email: str, db: Session = Depends(get_db)): - try: - user = db.query(User).filter(User.username == email).first() - if user: - # Generate password reset token - user_service = UserService(db) - reset_token = user_service.generate_reset_token() - user.reset_token = reset_token - user.reset_token_expires = datetime.utcnow() + timedelta(hours=24) - db.commit() - - # TODO: Send reset email with token - # For now, just return success message - return JSONResponse( - content={"message": "Password reset link has been sent to your email address"} - ) - else: - # To prevent user enumeration, show the same message - return JSONResponse( - content={"message": "Password reset link has been sent to your email address"} - ) - except Exception as e: - raise HTTPException( - status_code=500, - detail="Error processing password reset request" - ) - -@app.on_event("startup") -async def startup_event(): - """Initialize VPN server on startup""" - initialize_vpn_server() - -@app.on_event("shutdown") -async def shutdown_event(): - """Shutdown VPN server on application shutdown""" - global vpn_server - if vpn_server and vpn_server.is_running: - await vpn_server.stop() - logger.log(LogLevel.INFO, LogCategory.SYSTEM, "app", "VPN server shut down.") - -if __name__ == '__main__': - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=7860) - - - \ No newline at end of file diff --git a/ca/ca.crt b/ca/ca.crt deleted file mode 100644 index 8be09f3be52f9a5c3434a921d15ab1ab2c88e3f7..0000000000000000000000000000000000000000 --- a/ca/ca.crt +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFEzCCAvugAwIBAgIUVVFA6g5tK6la3NhsAuNIHSgtKcIwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAwwOT3V0bGluZSBWUE4gQ0EwHhcNMjUwODExMjA0MzMyWhcN -MzUwODA5MjA0MzMyWjAZMRcwFQYDVQQDDA5PdXRsaW5lIFZQTiBDQTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBANV3/kuWVymGqMMtjvKZRNgMFoQqCOMG -nAJi7bTehIFgd+uRmKnDwoHvtYjkGEmsY6D4Mq3Z6OxL7Wp3Ny2h1CouTQGdpR+I -sTvJEoVVNO8dNxJp9TrAXX3FQkSbW4+c7vfImZwoOfxMTu8AZ+VcI3+XeWKmyxdg -YgQMcTbXAMqtmIcDPxkM+/llniT4SfddG+J2MrPRuEEBcrr0vhRVPqTZXoBZ0XeM -TnLb4b7rcyo6L/MhrT07dBiDWt3J96RSM8uz/24pEFbQk3J3ppVGTPp0tpbL6DOB -3fijvOW+/Dtcq3pUiS6baM07bzD9czHatOq7Z2LbZshrrkxckl0Z83tHAvWvUOM+ -DAp+7FhxaxJQfHodKc5/QX4taEDR8iJj7uNe3tTE6gIIHlw+9oB6JN1RwSm/WQjW -QlZDrLXG9rCH6Rye2nRchIsYthDhjo1Q/FWEe14sIAxio/GKhPDK+GqWypSqbb1D -dcTQ0xduwXaP0DpA+ZJCLbZ+siX9tFyHW23YmhG14tP9IE2O9JsrFGDXDwLB4sAA -/DN+InQdW1SABFZk4TChcUs0BvaRuitzfsmiMFF1nR9Cytu3m82+FLSgFcDTNJv/ -n/nFE9/3eMOWwu786JDqJ/oAmnqRP+0qpfepMWTxbUJuhtAXpFlmjSTiiDhoyO// -w925yr1PUW8xAgMBAAGjUzBRMB0GA1UdDgQWBBRcrKe+jD6bZWSXI//HpXairOb6 -5zAfBgNVHSMEGDAWgBRcrKe+jD6bZWSXI//HpXairOb65zAPBgNVHRMBAf8EBTAD -AQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAr7zGUZ4golbeNUlwf//Ex08/PT+wHo93C -A2yqnRE5aX4HcGSHBdXnfJIajGzqg1ha6G9KoOAeUwrz43MKQJ5MlTPsstw3F5eG -PTf8pvXtqoKCUz+n5NflVAnPi2rkwg3xek3FNnRxeFtRNQk7JdiZyUrrkls9zdXG -b51nabQulMyU+S2AgS8tZieis9zKtaxL6FIkw/Ppd3kkC+IKSDqWq73UKesUwmwz -wVxmAjCwGNkzQmIriAEMOrjrUU4TqTlcYELmKjWjQtFn3DHYx0dVtQ+EtXvpvQTh -5y5E//O5crj8w8APWZ9AbV+RnHEzzUmn+CWEmKlw6INc5GipGJiF4iOgyIBKKTg0 -gfomG2nFrmmiQMLO+SMONdb6qWbdUMO8tptATV5+NEyjgcyoD4zhiQIjnDovil5F -klX8IwKMWSu7969uzXqNbr0gNFzUbpRtaoVoVJbaZMgDBSA+eWVJYlcDgE+qR6bE -NWTJiXZfVE3rat/pp/Nn25UHdtOBt1ZLGOItI7saB9xHqfS+aL+/Kj+mp8f3Tb/q -A20AeJUECG2jjErLYdUWY8+jMcPK6H34boUBuvmcdPRfBREsfotk0bNLiY+NlcnO -qCslUskUdquWVjYZ4Vce22byIJz2TdHWqfek4HAmU5p7VdKZ9Ux82pggWSHlEmUD -7YNttdciIA== ------END CERTIFICATE----- diff --git a/ca/ca.key b/ca/ca.key deleted file mode 100644 index 6305cd920da8993edcc5ebee2ddcf6d220c1a23a..0000000000000000000000000000000000000000 --- a/ca/ca.key +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDVd/5LllcphqjD -LY7ymUTYDBaEKgjjBpwCYu203oSBYHfrkZipw8KB77WI5BhJrGOg+DKt2ejsS+1q -dzctodQqLk0BnaUfiLE7yRKFVTTvHTcSafU6wF19xUJEm1uPnO73yJmcKDn8TE7v -AGflXCN/l3lipssXYGIEDHE21wDKrZiHAz8ZDPv5ZZ4k+En3XRvidjKz0bhBAXK6 -9L4UVT6k2V6AWdF3jE5y2+G+63MqOi/zIa09O3QYg1rdyfekUjPLs/9uKRBW0JNy -d6aVRkz6dLaWy+gzgd34o7zlvvw7XKt6VIkum2jNO28w/XMx2rTqu2di22bIa65M -XJJdGfN7RwL1r1DjPgwKfuxYcWsSUHx6HSnOf0F+LWhA0fIiY+7jXt7UxOoCCB5c -PvaAeiTdUcEpv1kI1kJWQ6y1xvawh+kcntp0XISLGLYQ4Y6NUPxVhHteLCAMYqPx -ioTwyvhqlsqUqm29Q3XE0NMXbsF2j9A6QPmSQi22frIl/bRch1tt2JoRteLT/SBN -jvSbKxRg1w8CweLAAPwzfiJ0HVtUgARWZOEwoXFLNAb2kborc37JojBRdZ0fQsrb -t5vNvhS0oBXA0zSb/5/5xRPf93jDlsLu/OiQ6if6AJp6kT/tKqX3qTFk8W1CbobQ -F6RZZo0k4og4aMjv/8Pducq9T1FvMQIDAQABAoICAF4EpfcjpYsQGIcyKxn1YGlp -VYdrPhPDhvXUHY7CTIjw9JBHxX3LzwDMk19R2tKj/xNYDXYdmiVswYnZLO/HrTrQ -vrDd/mp3mVvUEPixkQlZjDZrfYsdS3AH78poxHhpraRrcSBiZTuWXlOMkbXmkWny -TI+jF6LZnAHdewWkx1/8+kdIqkM9wULUO0VcJ7OvigcBeQ5S6XyUBzSJc6hf7SHM -7P7J0GR/YtPavUAJ0mTAUPscE4F7DIR5Yg16FTyFyfNHeVJK+rvJzI8nXLK1TlUn -D342G7SH17xZXWqw5cW9aHcOAYeKAiwWJ8BjeJd2FKWn2X6kVE4kgxV11i70LZSJ -eRtp3oEsUXOyTBu1XnVqkyokhvT4SW+UzCSbTVpCaDVbN223Ep2sw2ntjfL9OKwc -DmZaHhwycDtHABlERBpom+IZofVHH6NBX7CMsTNrD4y/qOSwdkbWrGhzGfNHBgrq -WPb9nSok+6m3LQT+8i7bpGj0LImYei5tp2rmQJt1psQOFTIksd7blOhy9dNeknj7 -zrfLXfT6GkeZ2CPStg7yUDTdQ7mVYa5FeTqpi09bfGZ9Y1ACxSdvdfMlrkLqTRQH -KkXKlwg607mK5t/QSt7tlUiE9AFCrciXviheQd2hfij2R5Kfoprp4PoGiWz/o478 -gsezT4CzugpEyZ/ICVqTAoIBAQD2jMVbqr5ivRsXdMhdru4MaZ5t/K+fHtL2V5c5 -SkDzrOIdOpy25zNbIpbNdSyVIv2zm69VTZHV3lDGT3RSFjr7T3jK1h9HrCBk9duV -IC1NDYNoIajJWGzF3VjAdRz5gsGNu74+04uNkkYBJzY4m6KYNsCYSPQw3Q01HdOh -hVcIbxyzbUFfUb2Nck+bnTYJAACnkCMxAZNE9g6THnTnjNMMwhuBvt19W/L4Qurl -F+Ix1kaBPe+3BFI4IyMhe3W3LvEraESDri5dTS0BlgM/lpGh9cme1JQc34xV/eUA -E31JG60prQCTWkkZuZxOTjgiiE7Z7/9L0hZqfcIVWHpF5gb3AoIBAQDdpp59OAaY -3Wud+lGc3KTkoiPHZYzAcVshUhg2FxkAKrwUJA7GGjcFWETh+E9LYYy0IfQcRKiN -ZmV8aXYnL26O/N6Dr2mHOt5WHvlzkEIZCp0QMJImh0If25fZXuMRl9eRVOQZn+Gh -eU8mhzNKS+5apXuofY04hch8VXCaGkwPVeD0EKXx9xuKcatGUCjQ1zkp7I9hXI69 -uX3pxZWHN2zCisFB9yEomq9G8xueUc86t0inFEsecESGLNOQmgxXscQTDt4tWG9h -7QQA1KA+TQeLg+i5Mr9xv++kTJNmPI63Jw0rWYvxk8mJRY5KUtSR+iSyYF960IJP -UlssOAFLMOkXAoIBAQDE6vpWlLErO87/lQ7ThHws/c7EGiZK+NuWVa862su11Edl -AQNaMp8aEy5PO184XpIzeg04HJR2NPJe8eb+CTNitb7MgujI3fmhqZyQJvsHp9tk -uD2PU0jNYFUaom9Z+c2N3n28wEmd8U5obWEpJWVgHZsGBn7C6Es8OW5me5Ff8x8B -UCn+b9LtvndG2vHljlL3gnAZHCD722sYpiLJLfkDH6XIoyFUlrQhBZGHGORY2cPG -RinIC3N/0tCkVW9Xt+53tPfEFMKDUri3o5FEoIYAzccTTMZfqUz1Aax9uxM96RUN -TFhBWMM6AL2O7Xp4WlZgSwelD09IDtmNIvXGDktRAoIBAQCt2h6+AM/L3wCmLM0O -yFHdsv91SsWXvFHKVOYApyVI6DwVYCLmZ3F4k7+TrnwjmCQQtgEOmxvJrOM1LlMq -cR26scSmbVPMafQygKEQb7ooghanuDEqXzUSX98+9BoOlpbSu08eejUzvj7C7ZDh -WaVfHCVeBvxZtTWHsExd0vqNnMKRLO28WCIV+QpqYD1jcSy5IX9k0oBzd6a3Ue7y -3BpGjScAYqJzgsCwWcbz6x8r4s7tnhE9krlstIRNC0dbEWfFuwexcYgLuyhEroHx -2+FrIM/NU2yt/+oraJTEwAMAzXSa5+XIWi7dqNzulwF8bkOSVd0OK7XKGcLBcDwz -ie2JAoIBAAEES8SwOYxbKxfle5zfEIJ33F9xvJZaoIHKK39oUsv6akG95QLuYi/O -EzNbvFs2DulZNijro9HavZzVTq8yevIsdJY3fUyVk/Rw408c8NiLPMqQs7CbuB43 -e8mexW0rECPM0BWJaBAbVbYfUyNPWFgAucmiPj8EKTiA64P/xHvwfeRF7cDsnBYl -ecAePGTGa0Kkrmbg1VbTxNu9Y5POdw/yYYfYw2D0dSX2g/wPovvcjdBemmdHBMr4 -UlL6YR/tc6/TuECC9FCR5Y/l9isd7v3lIanQKgQBG1+NkpIZ2rW/zDeBscAUGlYw -Xt2St+RJucRsevZirIfm7k2Dwyn22NY= ------END PRIVATE KEY----- diff --git a/config/outline_config.json b/config/outline_config.json deleted file mode 100644 index 7c0926ec5f78b0ea9f7186db3749f309182ac9a7..0000000000000000000000000000000000000000 --- a/config/outline_config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "server": { - "port": 443, - "cipher": "chacha20-ietf-poly1305", - "timeout": 600, - "workers": 4, - "bind_address": "0.0.0.0", - "access_key_salt": "974edfe0ae852e69d07515fa724334668cea0b34b83c5e4e5f6c83c0fe4fd3ed" - }, - "users": [] -} \ No newline at end of file diff --git a/config/server.json b/config/server.json deleted file mode 100644 index c7ca95799179a0c813c708e5e130ef39ef695ef8..0000000000000000000000000000000000000000 --- a/config/server.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "server": { - "host": "0.0.0.0", - "port": 8388, - "interface": "eth0", - "virtual_network": "10.0.0.0/16" - }, - "protocols": { - "shadowsocks": true, - "l2tp": true, - "pptp": true - }, - "offline_access": { - "enabled": true, - "timeout": 604800, - "max_offline_sessions": 5 - }, - "internet_sharing": { - "enabled": true, - "rate_limit": "10mbps", - "allowed_ports": ["80", "443", "53", "22"] - }, - "security": { - "encryption": "aes-256-gcm", - "ipsec_psk": "your_pre_shared_key_here", - "certificate_path": "/path/to/server.crt", - "key_path": "/path/to/server.key" - }, - "logging": { - "level": "info", - "file": "/var/log/vpn-server.log" - } -} diff --git a/config/server_config.json b/config/server_config.json deleted file mode 100644 index dbd1f54137349cca99967963380025e741ef79f3..0000000000000000000000000000000000000000 --- a/config/server_config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "server": { - "host": "0.0.0.0", - "port": 8443, - "virtual_network": "10.0.0.0/24", - "dns_servers": [ - "8.8.8.8", - "8.8.4.4" - ] - } -} \ No newline at end of file diff --git a/config/users.json b/config/users.json deleted file mode 100644 index 8d292253e8c0a7ef1a82fa07910d8dcc5334c626..0000000000000000000000000000000000000000 --- a/config/users.json +++ /dev/null @@ -1 +0,0 @@ -{"test@example.com": {"password": "$2b$12$r7y/a..c8zrgBL/nlN1yQOCDYxNkJu/I8iQzBxBpTzxndZ16TIiuC", "created_at": "2025-08-18T16:26:15.307068", "config_id": "e58fa5bc-30b0-4c16-9533-3761727a56fa"}} \ No newline at end of file diff --git a/core/__pycache__/database.cpython-311.pyc b/core/__pycache__/database.cpython-311.pyc deleted file mode 100644 index 08ea8ed6b38ddd31b1ad91dd3bdf71b219144c80..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/database.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/database_init.cpython-311.pyc b/core/__pycache__/database_init.cpython-311.pyc deleted file mode 100644 index 3233134c2c74b006b4054ede2618f3ae05123e33..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/database_init.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/ikev2_server.cpython-311.pyc b/core/__pycache__/ikev2_server.cpython-311.pyc deleted file mode 100644 index 2ddb748e3ab5f6b3ac8c708982de618ee3f633af..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/ikev2_server.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/ip_parser.cpython-311.pyc b/core/__pycache__/ip_parser.cpython-311.pyc deleted file mode 100644 index 5399e23bd0cb313aa56900f6e186808f42e32016..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/ip_parser.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/logger.cpython-311.pyc b/core/__pycache__/logger.cpython-311.pyc deleted file mode 100644 index 81be317f90a04bb94260aa8c4aebeeded78a15ca..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/logger.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/nat_engine.cpython-311.pyc b/core/__pycache__/nat_engine.cpython-311.pyc deleted file mode 100644 index 46ef44cf8f4496144be3e2968c72da88673e593c..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/nat_engine.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/outline_config.cpython-311.pyc b/core/__pycache__/outline_config.cpython-311.pyc deleted file mode 100644 index c7e258b54b6fab508fcf40b7d000bea0dab50aec..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/outline_config.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/outline_server.cpython-311.pyc b/core/__pycache__/outline_server.cpython-311.pyc deleted file mode 100644 index 0203b64f7097abf54533f90e7c66be03df118807..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/outline_server.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/port_manager.cpython-311.pyc b/core/__pycache__/port_manager.cpython-311.pyc deleted file mode 100644 index a963dd051178ed24bcae44a9ee3ee83dd173949b..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/port_manager.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/session_tracker.cpython-311.pyc b/core/__pycache__/session_tracker.cpython-311.pyc deleted file mode 100644 index 4cee1e3f921ed1b0807d8a86c6a9177fe58c36bf..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/session_tracker.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/shadowsocks_protocol.cpython-311.pyc b/core/__pycache__/shadowsocks_protocol.cpython-311.pyc deleted file mode 100644 index 20a7fb3ab33f64d401ad0f8dbc29a15a202b4fa8..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/shadowsocks_protocol.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/tcp_engine.cpython-311.pyc b/core/__pycache__/tcp_engine.cpython-311.pyc deleted file mode 100644 index 389053e81f78739f0237e9121deb5c1a6ffa519a..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/tcp_engine.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/tcp_forward.cpython-311.pyc b/core/__pycache__/tcp_forward.cpython-311.pyc deleted file mode 100644 index f5a342d80184fa6fae6d6609c10fbabdfcc60818..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/tcp_forward.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/traffic_router.cpython-311.pyc b/core/__pycache__/traffic_router.cpython-311.pyc deleted file mode 100644 index d5d6200369f3792d022882b45f95f20ab7391e17..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/traffic_router.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/vpn_auth.cpython-311.pyc b/core/__pycache__/vpn_auth.cpython-311.pyc deleted file mode 100644 index 9b82098d02bc603fe735907a6ec857c56ca8e154..0000000000000000000000000000000000000000 Binary files a/core/__pycache__/vpn_auth.cpython-311.pyc and /dev/null differ diff --git a/core/auth.py b/core/auth.py deleted file mode 100644 index 51131137138f1ccea879803662f869a138af7abd..0000000000000000000000000000000000000000 --- a/core/auth.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Authentication and authorization utilities -""" -from fastapi import Depends, HTTPException, status -from fastapi.security import OAuth2PasswordBearer -from jose import JWTError, jwt -from sqlalchemy.orm import Session -from datetime import datetime, timedelta -from typing import Optional - -from core.database import get_db -from models.user import User, UserRole -from schemas.auth import TokenData - -# JWT settings -SECRET_KEY = "your-secret-key" # Change this to a secure secret key in production -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - -def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): - """Create JWT token""" - to_encode = data.copy() - if expires_delta: - expire = datetime.utcnow() + expires_delta - else: - expire = datetime.utcnow() + timedelta(minutes=15) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt - -async def get_current_user( - token: str = Depends(oauth2_scheme), - db: Session = Depends(get_db) -) -> User: - """Get current user from JWT token""" - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") - if username is None: - raise credentials_exception - token_data = TokenData(username=username) - except JWTError: - raise credentials_exception - - user = db.query(User).filter(User.username == token_data.username).first() - if user is None: - raise credentials_exception - return user - -async def get_current_active_user( - current_user: User = Depends(get_current_user) -) -> User: - """Get current active user""" - if not current_user.is_active: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user - -async def get_current_admin_user( - current_user: User = Depends(get_current_active_user) -) -> User: - """Get current admin user""" - if current_user.role != UserRole.ADMIN: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Not enough privileges" - ) - return current_user diff --git a/core/database.py b/core/database.py deleted file mode 100644 index 83684ff6bbecce1d4b1048485af457646d45eb0b..0000000000000000000000000000000000000000 --- a/core/database.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Database configuration and base models -""" -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker -import os - -# Get the database file path -DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'vpn.db') -os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) - -# Create database engine -SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_PATH}" -engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} -) - -# Create SessionLocal class -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) - -# Create Base class -Base = declarative_base() - -def get_db(): - """Dependency to get database session""" - db = SessionLocal() - try: - yield db - finally: - db.close() diff --git a/core/database_init.py b/core/database_init.py deleted file mode 100644 index a32507ef0897efe51ae879655137e4905a127306..0000000000000000000000000000000000000000 --- a/core/database_init.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Database initialization and migration -""" -from sqlalchemy import event -from .database import Base, engine, SessionLocal -from .models.user import User, UserRole, UserStatus -import logging - -logger = logging.getLogger(__name__) - -def init_db(): - """Initialize database and create default admin user""" - try: - # Create all tables - Base.metadata.create_all(bind=engine) - - # Create a session - db = SessionLocal() - - # Check if admin user exists - admin = db.query(User).filter(User.role == UserRole.ADMIN).first() - if not admin: - # Create default admin user with password 'admin' - # Note: This should be changed immediately after first login - admin = User( - username="admin", - password_hash=User.hash_password("admin"), - role=UserRole.ADMIN, - status=UserStatus.ACTIVE - ) - db.add(admin) - db.commit() - logger.info("Created default admin user") - - db.close() - logger.info("Database initialized successfully") - return True - - except Exception as e: - logger.error(f"Error initializing database: {e}") - return False - -@event.listens_for(User, 'after_insert') -def user_created(mapper, connection, target): - """Log user creation""" - logger.info(f"New user created: {target.username}") - -@event.listens_for(User.status, 'set') -def user_status_changed(target, value, oldvalue, initiator): - """Log user status changes""" - if oldvalue and value != oldvalue: - logger.info(f"User {target.username} status changed from {oldvalue} to {value}") diff --git a/core/error_handlers.py b/core/error_handlers.py deleted file mode 100644 index c8c00c1562d3a96d2ad497edddb6c575685616f7..0000000000000000000000000000000000000000 --- a/core/error_handlers.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Global error handlers and exceptions -""" -from fastapi import FastAPI, Request, status -from fastapi.responses import JSONResponse -from sqlalchemy.exc import SQLAlchemyError -from typing import Union, Dict, Any - -class AppException(Exception): - """Base application exception""" - def __init__( - self, - status_code: int, - detail: Union[str, Dict[str, Any]], - headers: Dict[str, str] = None - ): - self.status_code = status_code - self.detail = detail - self.headers = headers - -def setup_error_handlers(app: FastAPI): - """Setup global error handlers""" - - @app.exception_handler(AppException) - async def app_exception_handler(request: Request, exc: AppException): - headers = exc.headers if exc.headers else {} - return JSONResponse( - status_code=exc.status_code, - content={"detail": exc.detail}, - headers=headers - ) - - @app.exception_handler(SQLAlchemyError) - async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError): - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content={ - "detail": "Database error occurred", - "message": str(exc) - } - ) - - @app.exception_handler(Exception) - async def general_exception_handler(request: Request, exc: Exception): - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content={ - "detail": "An unexpected error occurred", - "message": str(exc) - } - ) diff --git a/core/ikev2_server.py b/core/ikev2_server.py deleted file mode 100644 index d4f1e3de0b854b8fba1311d73b282b12f972aded..0000000000000000000000000000000000000000 --- a/core/ikev2_server.py +++ /dev/null @@ -1,207 +0,0 @@ -""" -IKEv2 Server Implementation for Outline VPN -""" - -import os -import subprocess -import tempfile -from typing import Dict, Optional -import uuid -from datetime import datetime, timedelta - -class IKEv2Server: - def __init__(self, server_ip: str, logger): - self.server_ip = server_ip - self.logger = logger - self.ca_dir = "ca" - self.cert_dir = "certs" - self.config_dir = "config" - self._setup_directories() - self._initialize_ca() - - def _setup_directories(self): - """Create necessary directories""" - for directory in [self.ca_dir, self.cert_dir, self.config_dir]: - os.makedirs(directory, exist_ok=True) - - def _initialize_ca(self): - """Initialize Certificate Authority if not already done""" - ca_key = os.path.join(self.ca_dir, "ca.key") - ca_cert = os.path.join(self.ca_dir, "ca.crt") - - if not os.path.exists(ca_key) or not os.path.exists(ca_cert): - # Generate CA private key - subprocess.run([ - "openssl", "genrsa", - "-out", ca_key, - "4096" - ], check=True) - - # Generate CA certificate - subprocess.run([ - "openssl", "req", - "-x509", - "-new", - "-nodes", - "-key", ca_key, - "-sha256", - "-days", "3650", - "-out", ca_cert, - "-subj", f"/CN=Outline VPN CA" - ], check=True) - - def generate_certificate(self, user_id: str) -> Dict[str, str]: - """Generate client certificate for IKEv2""" - cert_name = f"client_{user_id}" - key_path = os.path.join(self.cert_dir, f"{cert_name}.key") - csr_path = os.path.join(self.cert_dir, f"{cert_name}.csr") - cert_path = os.path.join(self.cert_dir, f"{cert_name}.crt") - p12_path = os.path.join(self.cert_dir, f"{cert_name}.p12") - - try: - # Generate client private key - subprocess.run([ - "openssl", "genrsa", - "-out", key_path, - "2048" - ], check=True) - - # Generate CSR - subprocess.run([ - "openssl", "req", - "-new", - "-key", key_path, - "-out", csr_path, - "-subj", f"/CN=client_{user_id}" - ], check=True) - - # Sign client certificate with CA - subprocess.run([ - "openssl", "x509", - "-req", - "-in", csr_path, - "-CA", os.path.join(self.ca_dir, "ca.crt"), - "-CAkey", os.path.join(self.ca_dir, "ca.key"), - "-CAcreateserial", - "-out", cert_path, - "-days", "365", - "-sha256" - ], check=True) - - # Create PKCS12 bundle - export_password = str(uuid.uuid4()) - subprocess.run([ - "openssl", "pkcs12", - "-export", - "-in", cert_path, - "-inkey", key_path, - "-out", p12_path, - "-password", f"pass:{export_password}" - ], check=True) - - # Read certificate files - with open(cert_path, 'r') as f: - cert_data = f.read() - with open(key_path, 'r') as f: - key_data = f.read() - with open(os.path.join(self.ca_dir, "ca.crt"), 'r') as f: - ca_data = f.read() - - return { - 'certificate': cert_data, - 'private_key': key_data, - 'ca_certificate': ca_data, - 'p12_bundle': p12_path, - 'p12_password': export_password - } - - except Exception as e: - self.logger.error("Error generating certificate: " + str(e)) - raise - - def generate_strongswan_config(self, user_id: str, psk: str) -> str: - """Generate strongSwan configuration for a user""" - config = f""" -conn outline-{user_id} - auto=add - compress=no - type=tunnel - keyexchange=ikev2 - fragmentation=yes - forceencaps=yes - - # Local/Server configuration - left=%any - leftsubnet=0.0.0.0/0 - leftcert=/etc/ipsec.d/certs/server.crt - leftsendcert=always - leftid=@outline.vpn - - # Remote/Client configuration - right=%any - rightid=%any - rightauth=eap-mschapv2 - rightsourceip=10.10.10.0/24 - rightdns=8.8.8.8,8.8.4.4 - - # Security parameters - ike=aes256-sha256-modp2048,aes128-sha1-modp2048 - esp=aes256-sha256,aes128-sha1 - dpdaction=clear - dpddelay=300s - rekey=no -""" - config_path = os.path.join(self.config_dir, f"outline-{user_id}.conf") - with open(config_path, 'w') as f: - f.write(config) - - return config_path - - def add_user(self, user_id: str, username: str, password: str, psk: str): - """Add a new VPN user""" - # Generate certificates - cert_data = self.generate_certificate(user_id) - - # Generate strongSwan config - config_path = self.generate_strongswan_config(user_id, psk) - - # Add user credentials to strongSwan secrets - secrets_path = os.path.join(self.config_dir, "ipsec.secrets") - with open(secrets_path, 'a') as f: - f.write(f'{username} : EAP "{password}"\n') - f.write(f'{self.server_ip} %any : PSK "{psk}"\n') - - return cert_data - - def remove_user(self, user_id: str): - """Remove a VPN user""" - # Remove certificates - cert_name = f"client_{user_id}" - for ext in ['.key', '.csr', '.crt', '.p12']: - path = os.path.join(self.cert_dir, f"{cert_name}{ext}") - if os.path.exists(path): - os.remove(path) - - # Remove config - config_path = os.path.join(self.config_dir, f"outline-{user_id}.conf") - if os.path.exists(config_path): - os.remove(config_path) - - # Remove from secrets (would need to rewrite the file) - # This is a bit more complex and would require parsing and rewriting ipsec.secrets - - - async def start(self): - """Start the IKEv2 service""" - self.logger.info("Starting IKEv2 service...") - # Placeholder for actual IKEv2 service startup logic - # This might involve starting strongSwan or similar - pass - - async def stop(self): - """Stop the IKEv2 service""" - self.logger.info("Stopping IKEv2 service...") - # Placeholder for actual IKEv2 service shutdown logic - pass - - diff --git a/core/ip_parser.py b/core/ip_parser.py deleted file mode 100644 index 079bae80f3a64e81771f1cd905a29059bbfeee66..0000000000000000000000000000000000000000 --- a/core/ip_parser.py +++ /dev/null @@ -1,230 +0,0 @@ -""" -IP Parser/Assembler Module - -Handles IPv4 packet parsing and construction: -- Parse IPv4, UDP, and TCP headers -- Calculate and verify checksums -- Handle packet fragmentation and reassembly -- Support various IP options -""" - -import struct -import socket -from typing import Dict, List, Optional, Tuple -from dataclasses import dataclass -from enum import Enum - - -class IPProtocol(Enum): - ICMP = 1 - TCP = 6 - UDP = 17 - - -@dataclass -class IPv4Header: - """IPv4 header structure""" - version: int = 4 - ihl: int = 5 # Internet Header Length (in 32-bit words) - tos: int = 0 # Type of Service - total_length: int = 0 - identification: int = 0 - flags: int = 0 # 3 bits: Reserved, Don't Fragment, More Fragments - fragment_offset: int = 0 # 13 bits - ttl: int = 64 # Time to Live - protocol: int = 0 - header_checksum: int = 0 - source_ip: str = '0.0.0.0' - dest_ip: str = '0.0.0.0' - options: bytes = b'' - - @property - def header_length(self) -> int: - """Get header length in bytes""" - return self.ihl * 4 - - @property - def dont_fragment(self) -> bool: - """Check if Don't Fragment flag is set""" - return bool(self.flags & 0x2) - - @property - def more_fragments(self) -> bool: - """Check if More Fragments flag is set""" - return bool(self.flags & 0x1) - - @property - def is_fragment(self) -> bool: - """Check if this is a fragment""" - return self.more_fragments or self.fragment_offset > 0 - - -@dataclass -class TCPHeader: - """TCP header structure""" - source_port: int = 0 - dest_port: int = 0 - seq_num: int = 0 - ack_num: int = 0 - data_offset: int = 5 # Header length in 32-bit words - reserved: int = 0 - flags: int = 0 # 9 bits: NS, CWR, ECE, URG, ACK, PSH, RST, SYN, FIN - window_size: int = 65535 - checksum: int = 0 - urgent_pointer: int = 0 - options: bytes = b'' - - @property - def header_length(self) -> int: - """Get header length in bytes""" - return self.data_offset * 4 - - # TCP Flag properties - @property - def fin(self) -> bool: - return bool(self.flags & 0x01) - - @property - def syn(self) -> bool: - return bool(self.flags & 0x02) - - @property - def rst(self) -> bool: - return bool(self.flags & 0x04) - - @property - def psh(self) -> bool: - return bool(self.flags & 0x08) - - @property - def ack(self) -> bool: - return bool(self.flags & 0x10) - - @property - def urg(self) -> bool: - return bool(self.flags & 0x20) - - @property - def ece(self) -> bool: - return bool(self.flags & 0x40) - - @property - def cwr(self) -> bool: - return bool(self.flags & 0x80) - - @property - def ns(self) -> bool: - return bool(self.flags & 0x100) - - -@dataclass -class UDPHeader: - """UDP header structure""" - source_port: int = 0 - dest_port: int = 0 - length: int = 8 # Header length (8) + data length - checksum: int = 0 - - -class IPParser: - """IP packet parser and assembler""" - - @staticmethod - def parse_ipv4_header(packet: bytes) -> Tuple[IPv4Header, bytes]: - """Parse IPv4 header and return header object and remaining data""" - # Basic header (20 bytes) - if len(packet) < 20: - raise ValueError("Packet too short for IPv4 header") - - ver_ihl, tos, total_len, ident, flags_frag, ttl, proto, checksum, src, dst = \ - struct.unpack('!BBHHHBBH4s4s', packet[:20]) - - version = ver_ihl >> 4 - ihl = ver_ihl & 0x0F - flags = flags_frag >> 13 - frag_offset = flags_frag & 0x1FFF - - header = IPv4Header( - version=version, - ihl=ihl, - tos=tos, - total_length=total_len, - identification=ident, - flags=flags, - fragment_offset=frag_offset, - ttl=ttl, - protocol=proto, - header_checksum=checksum, - source_ip=socket.inet_ntoa(src), - dest_ip=socket.inet_ntoa(dst) - ) - - # Extract options if present - header_len = header.header_length - if header_len > 20: - header.options = packet[20:header_len] - - return header, packet[header_len:] - - @staticmethod - def parse_tcp_header(packet: bytes) -> Tuple[TCPHeader, bytes]: - """Parse TCP header and return header object and remaining data""" - if len(packet) < 20: - raise ValueError("Packet too short for TCP header") - - src_port, dst_port, seq, ack, offset_flags, window, checksum, urgent = \ - struct.unpack('!HHIIHHH', packet[:20]) - - data_offset = offset_flags >> 12 - flags = offset_flags & 0x1FF - - header = TCPHeader( - source_port=src_port, - dest_port=dst_port, - seq_num=seq, - ack_num=ack, - data_offset=data_offset, - flags=flags, - window_size=window, - checksum=checksum, - urgent_pointer=urgent - ) - - # Extract options if present - header_len = header.header_length - if header_len > 20: - header.options = packet[20:header_len] - - return header, packet[header_len:] - - @staticmethod - def parse_udp_header(packet: bytes) -> Tuple[UDPHeader, bytes]: - """Parse UDP header and return header object and remaining data""" - if len(packet) < 8: - raise ValueError("Packet too short for UDP header") - - src_port, dst_port, length, checksum = struct.unpack('!HHHH', packet[:8]) - - header = UDPHeader( - source_port=src_port, - dest_port=dst_port, - length=length, - checksum=checksum - ) - - return header, packet[8:] - - @staticmethod - def calculate_checksum(data: bytes) -> int: - """Calculate IP/TCP/UDP checksum""" - if len(data) % 2 == 1: - data += b'\0' - - words = struct.unpack('!%dH' % (len(data) // 2), data) - checksum = sum(words) - - # Fold 32-bit sum into 16 bits - while checksum >> 16: - checksum = (checksum & 0xFFFF) + (checksum >> 16) - - return ~checksum & 0xFFFF diff --git a/core/l2tp_server.py b/core/l2tp_server.py deleted file mode 100644 index d2183889664ca2427e2f6b42e5786d3ef8feb8e3..0000000000000000000000000000000000000000 --- a/core/l2tp_server.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -L2TP/IPsec Server Implementation -Handles L2TP tunneling with IPsec encryption -""" - -import asyncio -import socket -import struct -from typing import Dict, Optional, Tuple -from dataclasses import dataclass -import os -import hmac -import hashlib -from .ip_parser import IPv4Header, IPParser -from .logger import Logger, LogCategory - -@dataclass -class L2TPSession: - tunnel_id: int - session_id: int - client_ip: str - assigned_ip: str - created_at: float - last_seen: float - bytes_in: int = 0 - bytes_out: int = 0 - -class L2TPServer: - """L2TP/IPsec server implementation""" - - def __init__(self, logger: Logger, ip_pool_start: str = "10.10.0.2"): - self.logger = logger - self.sessions: Dict[Tuple[int, int], L2TPSession] = {} # (tunnel_id, session_id) -> session - self.next_tunnel_id = 1 - self.next_session_id = 1 - self.next_ip = ip_pool_start - self._running = False - self._transport = None - self._ipsec = IPSecHandler(logger) - - async def start(self, host: str = "0.0.0.0", port: int = 1701): - """Start L2TP server""" - loop = asyncio.get_running_loop() - self._transport, _ = await loop.create_datagram_endpoint( - lambda: L2TPProtocol(self), - local_addr=(host, port) - ) - self._running = True - self.logger.info(LogCategory.SYSTEM, "l2tp_server", f"L2TP server started on {host}:{port}") - - async def stop(self): - """Stop L2TP server""" - if self._transport: - self._transport.close() - self._running = False - self.logger.info(LogCategory.SYSTEM, "l2tp_server", "L2TP server stopped") - - def allocate_ip(self) -> str: - """Allocate next available IP from pool""" - allocated = self.next_ip - # Increment last octet, handling rollover - last_octet = int(self.next_ip.split('.')[-1]) - if last_octet >= 254: - raise ValueError("IP pool exhausted") - self.next_ip = f"10.10.0.{last_octet + 1}" - return allocated - - async def handle_packet(self, data: bytes, addr: Tuple[str, int]): - """Handle incoming L2TP packet""" - try: - # Decrypt IPsec if present - if self._ipsec.is_ipsec_packet(data): - data = self._ipsec.decrypt_packet(data) - if not data: - return - - # Parse L2TP header - if len(data) < 6: - return - - flags = struct.unpack('!H', data[0:2])[0] - tunnel_id = struct.unpack('!H', data[2:4])[0] - session_id = struct.unpack('!H', data[4:6])[0] - - # Handle control messages - if flags & 0x8000: # Control message - await self._handle_control(data[6:], tunnel_id, session_id, addr) - else: # Data message - await self._handle_data(data[6:], tunnel_id, session_id) - - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "l2tp_server", f"Error handling packet: {e}") - - async def _handle_control(self, data: bytes, tunnel_id: int, session_id: int, addr: Tuple[str, int]): - """Handle L2TP control message""" - msg_type = struct.unpack('!H', data[0:2])[0] - - if msg_type == 1: # SCCRQ - Start-Control-Connection-Request - # Send SCCRP - Start-Control-Connection-Reply - reply = self._build_sccrp(tunnel_id) - await self._send_control(reply, addr) - - elif msg_type == 3: # ICRQ - Incoming-Call-Request - # Create new session - session = L2TPSession( - tunnel_id=tunnel_id, - session_id=self.next_session_id, - client_ip=addr[0], - assigned_ip=self.allocate_ip(), - created_at=asyncio.get_running_loop().time(), - last_seen=asyncio.get_running_loop().time() - ) - self.sessions[(tunnel_id, session_id)] = session - self.next_session_id += 1 - - # Send ICRP - Incoming-Call-Reply - reply = self._build_icrp(tunnel_id, session_id) - await self._send_control(reply, addr) - - async def _handle_data(self, data: bytes, tunnel_id: int, session_id: int): - """Handle L2TP data message""" - session_key = (tunnel_id, session_id) - if session_key not in self.sessions: - return - - session = self.sessions[session_key] - session.last_seen = asyncio.get_running_loop().time() - - # Parse PPP frame - if len(data) < 4: - return - - ppp_protocol = struct.unpack('!H', data[2:4])[0] - if ppp_protocol == 0x0021: # IP - ip_packet = data[4:] - await self._handle_ip_packet(ip_packet, session) - - async def _handle_ip_packet(self, data: bytes, session: L2TPSession): - """Handle IP packet inside PPP frame""" - try: - # Parse IP header - ip_header = IPParser.parse_ipv4_header(data) - - # Update statistics - session.bytes_in += len(data) - - # Forward packet to destination - if ip_header.protocol == socket.IPPROTO_TCP: - await self._forward_tcp(data, session) - elif ip_header.protocol == socket.IPPROTO_UDP: - await self._forward_udp(data, session) - - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "l2tp_server", f"Error handling IP packet: {e}") - - async def _forward_tcp(self, data: bytes, session: L2TPSession): - """Forward TCP packet""" - # This will be handled by the TCP forwarding engine - # Just a placeholder for now - pass - - async def _forward_udp(self, data: bytes, session: L2TPSession): - """Forward UDP packet""" - # This will be handled by the UDP forwarding engine - # Just a placeholder for now - pass - -class IPSecHandler: - """Handles IPsec encryption/decryption""" - - def __init__(self, logger: Logger): - self.logger = logger - self.security_associations: Dict[str, Dict] = {} - - def is_ipsec_packet(self, data: bytes) -> bool: - """Check if packet is IPsec encrypted""" - if len(data) < 8: - return False - # Check for ESP or AH protocol - return data[0] == 50 or data[0] == 51 - - def decrypt_packet(self, data: bytes) -> Optional[bytes]: - """Decrypt IPsec packet""" - try: - # Implementation depends on the encryption method - # This is a placeholder for actual decryption - return data[8:] # Skip ESP header for now - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "ipsec_handler", f"Decryption error: {e}") - return None - - def encrypt_packet(self, data: bytes, sa_id: str) -> bytes: - """Encrypt packet using IPsec""" - try: - # Implementation depends on the encryption method - # This is a placeholder for actual encryption - esp_header = struct.pack('!II', 0, 0) # SPI and Sequence Number - return esp_header + data - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "ipsec_handler", f"Encryption error: {e}") - return data diff --git a/core/logger.py b/core/logger.py deleted file mode 100644 index 26919427fc6c36cba2ec039ce1820dd3e1dd0d47..0000000000000000000000000000000000000000 --- a/core/logger.py +++ /dev/null @@ -1,282 +0,0 @@ -""" -Logger Module - -Centralized logging system for the virtual ISP stack: -- Structured logging with multiple levels -- Log aggregation and filtering -- Real-time log streaming -- Log persistence and rotation -""" - -import logging -import logging.handlers -import time -import threading -import json -import os -from typing import Dict, List, Optional, Any, Callable -from dataclasses import dataclass, asdict -from enum import Enum -from collections import deque -import queue - - -class LogLevel(Enum): - DEBUG = "DEBUG" - INFO = "INFO" - WARNING = "WARNING" - ERROR = "ERROR" - CRITICAL = "CRITICAL" - - -class LogCategory(Enum): - SYSTEM = "SYSTEM" - OUTLINE = "OUTLINE" - NAT = "NAT" - TCP = "TCP" - ROUTER = "ROUTER" - BRIDGE = "BRIDGE" - SOCKET = "SOCKET" - SESSION = "SESSION" - SECURITY = "SECURITY" - PERFORMANCE = "PERFORMANCE" - - -@dataclass -class LogEntry: - """Structured log entry""" - timestamp: float - level: str - category: str - module: str - message: str - session_id: Optional[str] = None - client_id: Optional[str] = None - source_ip: Optional[str] = None - dest_ip: Optional[str] = None - protocol: Optional[str] = None - metadata: Dict[str, Any] = None - - def __post_init__(self): - if self.timestamp == 0: - self.timestamp = time.time() - if self.metadata is None: - self.metadata = {} - - def to_dict(self) -> Dict: - """Convert to dictionary""" - return asdict(self) - - def to_json(self) -> str: - """Convert to JSON string""" - return json.dumps(self.to_dict(), default=str) - - -class LogFilter: - """Log filtering class""" - - def __init__(self): - self.level_filter: Optional[LogLevel] = None - self.category_filter: Optional[LogCategory] = None - self.module_filter: Optional[str] = None - self.session_filter: Optional[str] = None - self.client_filter: Optional[str] = None - self.ip_filter: Optional[str] = None - self.text_filter: Optional[str] = None - self.time_range: Optional[tuple] = None - - def matches(self, entry: LogEntry) -> bool: - """Check if log entry matches filter criteria""" - # Level filter - if self.level_filter: - entry_level_value = getattr(logging, entry.level) - filter_level_value = getattr(logging, self.level_filter.value) - if entry_level_value < filter_level_value: - return False - - # Category filter - if self.category_filter and entry.category != self.category_filter.value: - return False - - # Module filter - if self.module_filter and entry.module != self.module_filter: - return False - - # Session filter - if self.session_filter and entry.session_id != self.session_filter: - return False - - # Client filter - if self.client_filter and entry.client_id != self.client_filter: - return False - - # IP filter - if self.ip_filter: - if not (entry.source_ip == self.ip_filter or - entry.dest_ip == self.ip_filter): - return False - - # Text filter - if self.text_filter: - if self.text_filter.lower() not in entry.message.lower(): - return False - - # Time range filter - if self.time_range: - start_time, end_time = self.time_range - if not (start_time <= entry.timestamp <= end_time): - return False - - return True - - -class LogAggregator: - """Aggregates and manages logs""" - - def __init__(self, max_entries: int = 10000): - self.entries: deque = deque(maxlen=max_entries) - self.subscribers: List[Callable[[LogEntry], None]] = [] - self.lock = threading.Lock() - - def add_entry(self, entry: LogEntry): - """Add a new log entry""" - with self.lock: - self.entries.append(entry) - # Notify subscribers - for subscriber in self.subscribers: - try: - subscriber(entry) - except Exception as e: - logging.error(f"Error notifying subscriber: {e}") - - def get_entries(self, log_filter: Optional[LogFilter] = None) -> List[LogEntry]: - """Get filtered log entries""" - with self.lock: - if log_filter is None: - return list(self.entries) - return [entry for entry in self.entries if log_filter.matches(entry)] - - def subscribe(self, callback: Callable[[LogEntry], None]): - """Subscribe to new log entries""" - with self.lock: - self.subscribers.append(callback) - - def unsubscribe(self, callback: Callable[[LogEntry], None]): - """Unsubscribe from log entries""" - with self.lock: - try: - self.subscribers.remove(callback) - except ValueError: - pass - - -class LogManager: - """Central logging manager""" - - _instance = None - _lock = threading.Lock() - - def __new__(cls): - with cls._lock: - if cls._instance is None: - cls._instance = super().__new__(cls) - cls._instance._initialized = False - return cls._instance - - def __init__(self): - if self._initialized: - return - - self.aggregator = LogAggregator() - self.log_queue = queue.Queue() - self.file_handler = None - self.console_handler = None - - # Configure logging - self._configure_logging() - - # Start processing thread - self.running = True - self.process_thread = threading.Thread(target=self._process_queue) - self.process_thread.daemon = True - self.process_thread.start() - - self._initialized = True - - def _configure_logging(self): - """Configure logging handlers""" - log_dir = os.path.join(os.path.dirname(__file__), '../logs') - os.makedirs(log_dir, exist_ok=True) - - # File handler with rotation - log_file = os.path.join(log_dir, 'outline.log') - self.file_handler = logging.handlers.RotatingFileHandler( - log_file, maxBytes=10*1024*1024, backupCount=5 - ) - self.file_handler.setFormatter( - logging.Formatter('%(asctime)s [%(levelname)s] %(message)s') - ) - - # Console handler - self.console_handler = logging.StreamHandler() - self.console_handler.setFormatter( - logging.Formatter('[%(levelname)s] %(message)s') - ) - - # Configure root logger - root_logger = logging.getLogger() - root_logger.setLevel(logging.INFO) - root_logger.addHandler(self.file_handler) - root_logger.addHandler(self.console_handler) - - def _process_queue(self): - """Process log queue""" - while self.running: - try: - entry = self.log_queue.get(timeout=1) - self.aggregator.add_entry(entry) - except queue.Empty: - continue - except Exception as e: - logging.error(f"Error processing log entry: {e}") - - def log(self, level: LogLevel, category: LogCategory, module: str, - message: str, **kwargs): - """Add a log entry""" - entry = LogEntry( - timestamp=time.time(), - level=level.value, - category=category.value, - module=module, - message=message, - **kwargs - ) - self.log_queue.put(entry) - - # Also log to Python's logging system - log_func = getattr(logging, level.value.lower()) - log_func(f"[{category.value}] {message}") - - def get_logs(self, log_filter: Optional[LogFilter] = None) -> List[LogEntry]: - """Get filtered logs""" - return self.aggregator.get_entries(log_filter) - - def subscribe(self, callback: Callable[[LogEntry], None]): - """Subscribe to log entries""" - self.aggregator.subscribe(callback) - - def unsubscribe(self, callback: Callable[[LogEntry], None]): - """Unsubscribe from log entries""" - self.aggregator.unsubscribe(callback) - - def shutdown(self): - """Shutdown the log manager""" - self.running = False - if self.process_thread.is_alive(): - self.process_thread.join() - - # Close handlers - if self.file_handler: - self.file_handler.close() - if self.console_handler: - self.console_handler.close() diff --git a/core/middleware.py b/core/middleware.py deleted file mode 100644 index 2f75e159dca63d3d06b8cbc61ac1c13839463798..0000000000000000000000000000000000000000 --- a/core/middleware.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Custom middleware for the application -""" -from fastapi import Request -from starlette.middleware.base import BaseHTTPMiddleware -from starlette.responses import Response -import logging -import time -from typing import Callable - -logger = logging.getLogger(__name__) - -class RequestLoggerMiddleware(BaseHTTPMiddleware): - """Middleware for logging requests""" - - async def dispatch(self, request: Request, call_next: Callable) -> Response: - start_time = time.time() - - # Get request details - method = request.method - url = str(request.url) - - try: - # Process the request - response = await call_next(request) - - # Calculate processing time - process_time = time.time() - start_time - - # Log the request details - logger.info( - f"Request: {method} {url} - Status: {response.status_code} - " - f"Processing Time: {process_time:.3f}s" - ) - - return response - - except Exception as e: - logger.error( - f"Request failed: {method} {url} - Error: {str(e)}" - ) - raise - -class ErrorHandlerMiddleware(BaseHTTPMiddleware): - """Middleware for handling errors""" - - async def dispatch(self, request: Request, call_next: Callable) -> Response: - try: - return await call_next(request) - except Exception as e: - # Log the error - logger.error(f"Unhandled error: {str(e)}", exc_info=True) - - # Return error response - return Response( - content={"detail": "Internal server error"}, - status_code=500 - ) diff --git a/core/models/__pycache__/user.cpython-311.pyc b/core/models/__pycache__/user.cpython-311.pyc deleted file mode 100644 index e6affde43b3c7b3a8bcaf7b99176563d508ceb3d..0000000000000000000000000000000000000000 Binary files a/core/models/__pycache__/user.cpython-311.pyc and /dev/null differ diff --git a/core/models/user.py b/core/models/user.py deleted file mode 100644 index 66ffaabf2c81f07a6e77bc69b4e44454a81e7499..0000000000000000000000000000000000000000 --- a/core/models/user.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -User models and authentication -""" -from datetime import datetime, timedelta -from typing import Optional, List -from sqlalchemy import Column, Integer, String, DateTime, Boolean, Enum as SQLEnum -from sqlalchemy.sql import func -import enum -from passlib.context import CryptContext -from core.database import Base - -# Password hashing context -pwd_context = CryptContext( - schemes=["argon2"], - deprecated="auto", - argon2__memory_cost=65536, - argon2__parallelism=4, - argon2__time_cost=3 -) - -class UserRole(enum.Enum): - ADMIN = "admin" - POWER_USER = "power_user" - USER = "user" - GUEST = "guest" - -class UserStatus(enum.Enum): - ACTIVE = "active" - SUSPENDED = "suspended" - LOCKED = "locked" - PENDING = "pending" - EXPIRED = "expired" - -class User(Base): - """User model for authentication and authorization""" - __tablename__ = "users" - - id = Column(Integer, primary_key=True, index=True) - username = Column(String, unique=True, index=True, nullable=False) - password_hash = Column(String, nullable=False) - role = Column(SQLEnum(UserRole), nullable=False, default=UserRole.USER) - status = Column(SQLEnum(UserStatus), nullable=False, default=UserStatus.PENDING) - - # Account tracking - created_at = Column(DateTime(timezone=True), server_default=func.now()) - last_login = Column(DateTime(timezone=True), nullable=True) - failed_attempts = Column(Integer, default=0) - lockout_until = Column(DateTime(timezone=True), nullable=True) - - def verify_password(self, password: str) -> bool: - """Verify password against hash""" - return pwd_context.verify(password, self.password_hash) - - @staticmethod - def hash_password(password: str) -> str: - """Hash password using Argon2""" - return pwd_context.hash(password) - - def is_locked(self) -> bool: - """Check if account is locked""" - if self.lockout_until and self.lockout_until > datetime.utcnow(): - return True - return False - - def record_login_attempt(self, success: bool): - """Record login attempt and handle lockout""" - if success: - self.failed_attempts = 0 - self.last_login = datetime.utcnow() - self.lockout_until = None - else: - self.failed_attempts += 1 - if self.failed_attempts >= 5: # Lock after 5 failed attempts - self.lockout_until = datetime.utcnow() + timedelta(minutes=15) - -class UserSession(Base): - """User session tracking""" - __tablename__ = "user_sessions" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, nullable=False) - token = Column(String, unique=True, index=True, nullable=False) - ip_address = Column(String) - device_info = Column(String) - created_at = Column(DateTime(timezone=True), server_default=func.now()) - expires_at = Column(DateTime(timezone=True), nullable=False) - last_active = Column(DateTime(timezone=True), server_default=func.now()) - -class UserPermission(Base): - """User specific permissions""" - __tablename__ = "user_permissions" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, nullable=False) - permission_type = Column(String, nullable=False) - resource_id = Column(String, nullable=True) # Optional resource identifier - granted_at = Column(DateTime(timezone=True), server_default=func.now()) - granted_by = Column(Integer, nullable=True) # User ID who granted the permission diff --git a/core/nat_engine.py b/core/nat_engine.py deleted file mode 100644 index 543c88ea19239a2a75e7473c7d6503ec83e51e30..0000000000000000000000000000000000000000 --- a/core/nat_engine.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -NAT Engine Module for VPN -Implements Network Address Translation -""" - -import asyncio -import time -from typing import Dict, Optional, Tuple -from dataclasses import dataclass -import logging -import subprocess - -logger = logging.getLogger(__name__) - -@dataclass -class NATSession: - virtual_ip: str - virtual_port: int - real_ip: str - real_port: int - host_port: int - created_time: float - last_activity: float - bytes_in: int = 0 - bytes_out: int = 0 - -class NATEngine: - def __init__(self, logger_instance=None): - self.sessions: Dict[str, NATSession] = {} - self.port_mappings: Dict[int, Tuple[str, int]] = {} - self.next_port = 10000 - self.cleanup_interval = 300 # 5 minutes - self._cleanup_task = None - self.logger = logger_instance if logger_instance else logging.getLogger(__name__) - - async def start(self): - """Start the NAT engine""" - self._cleanup_task = asyncio.create_task(self._cleanup_loop()) - self.logger.info("NAT engine started") - - async def stop(self): - """Stop the NAT engine""" - if self._cleanup_task: - self._cleanup_task.cancel() - try: - await self._cleanup_task - except asyncio.CancelledError: - pass - self.sessions.clear() - self.port_mappings.clear() - self.logger.info("NAT engine stopped") - - def create_session(self, virtual_ip: str, virtual_port: int, - real_ip: str, real_port: int) -> NATSession: - """Create a new NAT session""" - host_port = self._allocate_port() - session = NATSession( - virtual_ip=virtual_ip, - virtual_port=virtual_port, - real_ip=real_ip, - real_port=real_port, - host_port=host_port, - created_time=time.time(), - last_activity=time.time() - ) - - session_key = f"{virtual_ip}:{virtual_port}" - self.sessions[session_key] = session - self.port_mappings[host_port] = (virtual_ip, virtual_port) - - self.logger.debug(f"Created NAT session: {session_key} -> {real_ip}:{real_port}") - return session - - def lookup_session(self, ip: str, port: int) -> Optional[NATSession]: - """Look up a NAT session by real IP and port""" - for session in self.sessions.values(): - if session.real_ip == ip and session.real_port == port: - return session - return None - - def get_session_by_virtual(self, ip: str, port: int) -> Optional[NATSession]: - """Get session by virtual IP and port""" - return self.sessions.get(f"{ip}:{port}") - - def remove_session(self, session: NATSession): - """Remove a NAT session""" - session_key = f"{session.virtual_ip}:{session.virtual_port}" - if session_key in self.sessions: - del self.sessions[session_key] - if session.host_port in self.port_mappings: - del self.port_mappings[session.host_port] - - def _allocate_port(self) -> int: - """Allocate a new port for NAT""" - while self.next_port in self.port_mappings: - self.next_port += 1 - if self.next_port > 65535: - self.next_port = 10000 - return self.next_port - - async def _cleanup_loop(self): - """Periodically clean up expired sessions""" - while True: - try: - await asyncio.sleep(self.cleanup_interval) - current_time = time.time() - - for session_key, session in list(self.sessions.items()): - if current_time - session.last_activity > self.cleanup_interval: - self.remove_session(session) - self.logger.debug(f"Removed expired session: {session_key}") - - except asyncio.CancelledError: - break - except Exception as e: - self.logger.error(f"Error in cleanup loop: {e}") - - def get_stats(self) -> Dict: - """Get NAT engine statistics""" - return { - "active_sessions": len(self.sessions), - "allocated_ports": len(self.port_mappings), - "total_bytes_in": sum(s.bytes_in for s in self.sessions.values()), - "total_bytes_out": sum(s.bytes_out for s in self.sessions.values()) - } - - - - - async def setup_nat(self, interface: str): - """Setup NAT configuration""" - # In this Windows implementation, NAT is handled at the application level - # through the traffic forwarding engine and doesn't require system-level configuration - logger.info(f"NAT configuration ready for interface {interface}") - pass diff --git a/core/outline_config.py b/core/outline_config.py deleted file mode 100644 index 7555713689c954c104739007ebf77e6f4720a683..0000000000000000000000000000000000000000 --- a/core/outline_config.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -Outline VPN Configuration Manager -""" - -import os -import json -from dataclasses import dataclass -from typing import List, Optional, Dict - -@dataclass -class OutlineConfig: - port: int = 443 - cipher: str = "chacha20-ietf-poly1305" - timeout: int = 600 - workers: int = 4 - bind_address: str = "0.0.0.0" - access_key_salt: str = os.urandom(32).hex() - -@dataclass -class UserConfig: - user_id: str - access_key: str - data_limit: Optional[int] = None - expiry_date: Optional[str] = None - is_active: bool = True - last_connection: Optional[str] = None - bandwidth_usage: int = 0 - -class OutlineManager: - def __init__(self, config_path: str = "config/outline_config.json"): - self.config_path = config_path - self.config = OutlineConfig() - self.users: List[UserConfig] = [] - self._load_config() - - def _load_config(self): - """Load configuration from file or create default""" - os.makedirs(os.path.dirname(self.config_path), exist_ok=True) - - if os.path.exists(self.config_path): - with open(self.config_path, 'r') as f: - data = json.load(f) - self.config = OutlineConfig(**data.get('server', {})) - self.users = [UserConfig(**u) for u in data.get('users', [])] - else: - self.save_config() - - def save_config(self): - """Save current configuration to file""" - data = { - 'server': self.config.__dict__, - 'users': [u.__dict__ for u in self.users] - } - with open(self.config_path, 'w') as f: - json.dump(data, f, indent=4) - - def add_user(self, user_id: str, data_limit: Optional[int] = None) -> UserConfig: - """Add a new user and generate their access key""" - # Check if user already exists - if any(u.user_id == user_id for u in self.users): - raise ValueError(f"User {user_id} already exists") - - access_key = self._generate_access_key(user_id) - user = UserConfig( - user_id=user_id, - access_key=access_key, - data_limit=data_limit - ) - self.users.append(user) - self.save_config() - return user - - def remove_user(self, user_id: str) -> bool: - """Remove a user by their ID""" - initial_length = len(self.users) - self.users = [u for u in self.users if u.user_id != user_id] - if len(self.users) < initial_length: - self.save_config() - return True - return False - - def get_user_by_key(self, access_key: str) -> Optional[UserConfig]: - """Find user by their access key""" - for user in self.users: - if user.access_key == access_key and user.is_active: - return user - return None - - def update_user_bandwidth(self, user_id: str, bytes_used: int): - """Update user's bandwidth usage""" - for user in self.users: - if user.user_id == user_id: - user.bandwidth_usage += bytes_used - if user.data_limit and user.bandwidth_usage >= user.data_limit: - user.is_active = False - self.save_config() - break - - def _generate_access_key(self, user_id: str) -> str: - """Generate a unique access key for a user""" - import hashlib - key = hashlib.sha256(f"{user_id}{self.config.access_key_salt}".encode()).hexdigest() - return key[:32] # Return first 32 chars as access key - - def get_server_stats(self) -> Dict: - """Get server statistics""" - return { - "total_users": len(self.users), - "active_users": sum(1 for u in self.users if u.is_active), - "total_bandwidth": sum(u.bandwidth_usage for u in self.users) - } - - -def generate_openvpn_certificates(config_id: str) -> Dict: - """Placeholder for OpenVPN certificate generation""" - return {"cert": "placeholder_cert", "key": "placeholder_key"} - -def generate_openvpn_config(config_id: str, server_ip: str) -> str: - """Placeholder for OpenVPN config generation""" - return f"client\ndev tun\nproto udp\nremote {server_ip} 1194\nresolv-retry infinite\nnobind\npersist-key\npersist-tun\nremote-cert-tls server\ncipher AES-256-CBC\nverb 3" - -def generate_wireguard_keys(config_id: str) -> Dict: - """Placeholder for WireGuard key generation""" - return {"server_public": "placeholder_server_public", "client_private": "placeholder_client_private", "client_public": "placeholder_client_public"} - - diff --git a/core/outline_server.py b/core/outline_server.py deleted file mode 100644 index 78a8e90e921443dc986fc83a032d4b3016172eb8..0000000000000000000000000000000000000000 --- a/core/outline_server.py +++ /dev/null @@ -1,181 +0,0 @@ -""" -Enhanced Outline VPN Server Implementation -Core server component with offline access and internet sharing support -""" - -import asyncio -import logging -import json -import os -import psutil -from typing import Dict, Optional, List, Tuple -from .shadowsocks_protocol import ShadowsocksProtocol -from .nat_engine import NATEngine -from .traffic_router import TrafficRouter -from .outline_config import OutlineManager, OutlineConfig -from .ikev2_server import IKEv2Server as IPsecManager -from .session_tracker import SessionTracker -from .port_manager import PortManager - -logger = logging.getLogger(__name__) - -class OutlineServer: - def __init__(self, config: Dict): - self.config = config - self.outline_manager = OutlineManager() - self.ipsec_manager = IPsecManager(config["server"]["host"], logger) - - # Initialize authentication - from .vpn_auth import VPNAuthManager - from .database_init import init_db - - # Initialize database - if not init_db(): - raise RuntimeError("Failed to initialize database") - - self.auth_manager = VPNAuthManager() - - # Initialize port manager - self.port_manager = PortManager() - - # Initialize components - self.nat_engine = NATEngine(logger) - self.traffic_router = TrafficRouter({ - "vpn_host": "0.0.0.0", # Listen on all interfaces - "vpn_port": config["server"]["port"], - "virtual_network": config["server"]["virtual_network"] - }, logger) - - # Initialize session tracker for offline support - self.session_tracker = SessionTracker() - - self.sessions = {} - self.is_running = False - self.bound_ports: List[Tuple[int, str]] = [] # List of (port, service) tuples - - async def setup_internet_sharing(self): - """Configure internet sharing for VPN clients""" - try: - interface = self.config["server"].get("interface", "eth0") - await self.nat_engine.setup_nat(interface) - logger.info(f"Internet sharing enabled on interface {interface}") - except Exception as e: - logger.error(f"Failed to setup internet sharing: {e}") - raise - async def start(self): - """Start the VPN server""" - if self.is_running: - logger.warning("Outline VPN server is already running") - return - - try: - # Clean up any existing connections first - await self.cleanup_existing_connections() - - # Setup network - await self.setup_internet_sharing() - - # Start components in order with proper error handling - try: - await self.nat_engine.start() - except Exception as e: - logger.error(f"Failed to start NAT engine: {e}") - raise - - try: - await self.traffic_router.start() - except Exception as e: - await self.nat_engine.stop() - logger.error(f"Failed to start traffic router: {e}") - raise - - try: - await self.ipsec_manager.start() - except Exception as e: - await self.nat_engine.stop() - await self.traffic_router.stop() - logger.error(f"Failed to start IKEv2 service: {e}") - raise - - self.is_running = True - logger.info(f"Outline VPN server started successfully on port {self.config['server']['port']}") - - except Exception as e: - logger.error(f"Failed to start server: {e}") - await self.stop() # Ensure cleanup on failure - raise - - async def cleanup_existing_connections(self): - """Clean up any existing connections before starting""" - import psutil - current_pid = os.getpid() - - # Find and clean up any existing VPN processes - for proc in psutil.process_iter(['pid', 'name', 'connections']): - try: - if proc.pid != current_pid: - for conn in proc.connections(): - if conn.laddr.port == self.config['server']['port']: - logger.warning(f"Found existing process using port {self.config['server']['port']}, terminating...") - proc.terminate() - proc.wait(timeout=5) - except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.TimeoutExpired): - continue - - async def stop(self): - """Stop the VPN server""" - self.is_running = False - await self.traffic_router.stop() - await self.nat_engine.stop() - await self.ipsec_manager.stop() - - # Close all active sessions - for session in self.sessions.values(): - await session.close() - self.sessions.clear() - - logger.info("VPN server stopped") - - async def _handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): - """Handle new client connections""" - peer = writer.get_extra_info('peername') - logger.info(f"New connection from {peer}") - - try: - # First packet contains access key - first_packet = await reader.read(64) - access_key = self._extract_access_key(first_packet) - - # Validate access key - user = self.outline_manager.get_user_by_key(access_key) - if not user: - logger.warning(f"Invalid access key from {peer}") - writer.close() - return - - # Create protocol handler - protocol = ShadowsocksProtocol(access_key) - self.sessions[user.user_id] = protocol - - # Handle the connection - await protocol.handle_connection(reader, writer) - - except Exception as e: - logger.error(f"Error handling client {peer}: {e}") - finally: - if not writer.is_closing(): - writer.close() - await writer.wait_closed() - - def _extract_access_key(self, packet: bytes) -> str: - """Extract access key from initial packet""" - return packet[:32].hex() - - def get_stats(self) -> Dict: - """Get server statistics""" - return { - "is_running": self.is_running, - "active_sessions": len(self.sessions), - "traffic_stats": self.traffic_router.get_stats(), - "nat_stats": self.nat_engine.get_stats() - } diff --git a/core/port_manager.py b/core/port_manager.py deleted file mode 100644 index 38ba668b51c26834dea078ec76a46f044cc4b66c..0000000000000000000000000000000000000000 --- a/core/port_manager.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -Port Manager for VPN Server -Handles port allocation, testing, and management -""" - -import socket -import asyncio -import logging -from typing import List, Optional, Tuple -from contextlib import closing - -logger = logging.getLogger(__name__) - -class PortManager: - # Common VPN ports to try - DEFAULT_VPN_PORTS = [ - 443, # HTTPS - 8388, # Shadowsocks default - 8443, # Alternative HTTPS - 1194, # OpenVPN default - 1984, # Outline default - 8000, # Alternative - 8080 # Alternative HTTP - ] - - def __init__(self): - self.bound_ports: List[int] = [] - self.reserved_ports: List[int] = [] - - async def find_available_port(self, preferred_port: int, - fallback_ports: List[int] = None, - bind_address: str = '0.0.0.0') -> Tuple[int, bool]: - """ - Find an available port, starting with preferred_port. - Returns: (port_number, is_preferred_port) - """ - # Try preferred port first - if await self._test_port(preferred_port, bind_address): - return preferred_port, True - - # Try fallback ports - ports_to_try = (fallback_ports or []) + self.DEFAULT_VPN_PORTS - for port in ports_to_try: - if port != preferred_port and await self._test_port(port, bind_address): - return port, False - - # If no predefined ports work, scan for any available port - port = await self._scan_for_available_port(bind_address) - return port, False - - async def _test_port(self, port: int, bind_address: str) -> bool: - """Test if a port is available for both TCP and UDP""" - if port in self.bound_ports or port in self.reserved_ports: - return False - - try: - # Test TCP - tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - tcp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - tcp_sock.bind((bind_address, port)) - tcp_sock.close() - - # Test UDP - udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - udp_sock.bind((bind_address, port)) - udp_sock.close() - - return True - - except OSError: - return False - - async def _scan_for_available_port(self, bind_address: str, - start_port: int = 10000, - end_port: int = 65535) -> int: - """Scan for any available port in the given range""" - for port in range(start_port, end_port): - if await self._test_port(port, bind_address): - return port - raise RuntimeError("No available ports found") - - async def reserve_port(self, port: int): - """Reserve a port for future use""" - if port not in self.bound_ports and port not in self.reserved_ports: - self.reserved_ports.append(port) - - async def release_port(self, port: int): - """Release a reserved or bound port""" - if port in self.bound_ports: - self.bound_ports.remove(port) - if port in self.reserved_ports: - self.reserved_ports.remove(port) - - async def bind_port(self, port: int, bind_address: str = '0.0.0.0') -> bool: - """ - Attempt to bind to a port and mark it as bound if successful - Returns True if binding was successful - """ - if await self._test_port(port, bind_address): - self.bound_ports.append(port) - if port in self.reserved_ports: - self.reserved_ports.remove(port) - return True - return False - - def get_bound_ports(self) -> List[int]: - """Get list of currently bound ports""" - return self.bound_ports.copy() - - def get_reserved_ports(self) -> List[int]: - """Get list of currently reserved ports""" - return self.reserved_ports.copy() diff --git a/core/pptp_server.py b/core/pptp_server.py deleted file mode 100644 index c258946282ad1f78ffa48f983fad2fc2c58eb480..0000000000000000000000000000000000000000 --- a/core/pptp_server.py +++ /dev/null @@ -1,179 +0,0 @@ -""" -PPTP Server Implementation -Handles PPTP tunneling and packet forwarding -""" - -import asyncio -import socket -import struct -from typing import Dict, Optional, Tuple -from dataclasses import dataclass -import os -import hmac -import hashlib -from .ip_parser import IPv4Header, IPParser -from .logger import Logger, LogCategory - -@dataclass -class PPTPSession: - call_id: int - peer_call_id: int - client_ip: str - assigned_ip: str - created_at: float - last_seen: float - bytes_in: int = 0 - bytes_out: int = 0 - -class PPTPServer: - """PPTP server implementation""" - - def __init__(self, logger: Logger, ip_pool_start: str = "10.20.0.2"): - self.logger = logger - self.sessions: Dict[int, PPTPSession] = {} # call_id -> session - self.next_call_id = 1 - self.next_ip = ip_pool_start - self._running = False - self._transport = None - - async def start(self, host: str = "0.0.0.0", port: int = 1723): - """Start PPTP server""" - loop = asyncio.get_running_loop() - self._transport, _ = await loop.create_server( - lambda: PPTPProtocol(self), - host, port - ) - self._running = True - self.logger.info(LogCategory.SYSTEM, "pptp_server", f"PPTP server started on {host}:{port}") - - async def stop(self): - """Stop PPTP server""" - if self._transport: - self._transport.close() - self._running = False - self.logger.info(LogCategory.SYSTEM, "pptp_server", "PPTP server stopped") - - def allocate_ip(self) -> str: - """Allocate next available IP from pool""" - allocated = self.next_ip - # Increment last octet, handling rollover - last_octet = int(self.next_ip.split('.')[-1]) - if last_octet >= 254: - raise ValueError("IP pool exhausted") - self.next_ip = f"10.20.0.{last_octet + 1}" - return allocated - - async def handle_packet(self, data: bytes, addr: Tuple[str, int]): - """Handle incoming PPTP packet""" - try: - if len(data) < 8: - return - - # Parse PPTP header - message_type = struct.unpack('!H', data[2:4])[0] - - if message_type == 1: # Control Message - await self._handle_control(data, addr) - else: # Data Message (GRE) - await self._handle_gre(data, addr) - - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "pptp_server", f"Error handling packet: {e}") - - async def _handle_control(self, data: bytes, addr: Tuple[str, int]): - """Handle PPTP control message""" - control_type = struct.unpack('!H', data[8:10])[0] - - if control_type == 1: # Start-Control-Connection-Request - # Send Start-Control-Connection-Reply - reply = self._build_start_control_reply() - await self._send_control(reply, addr) - - elif control_type == 7: # Outgoing-Call-Request - call_id = struct.unpack('!H', data[12:14])[0] - - # Create new session - session = PPTPSession( - call_id=call_id, - peer_call_id=self.next_call_id, - client_ip=addr[0], - assigned_ip=self.allocate_ip(), - created_at=asyncio.get_running_loop().time(), - last_seen=asyncio.get_running_loop().time() - ) - self.sessions[call_id] = session - self.next_call_id += 1 - - # Send Outgoing-Call-Reply - reply = self._build_outgoing_call_reply(session) - await self._send_control(reply, addr) - - async def _handle_gre(self, data: bytes, addr: Tuple[str, int]): - """Handle GRE encapsulated data""" - try: - if len(data) < 12: # GRE v1 header size - return - - # Parse GRE header - flags = struct.unpack('!H', data[0:2])[0] - protocol = struct.unpack('!H', data[2:4])[0] - payload_len = struct.unpack('!H', data[4:6])[0] - call_id = struct.unpack('!H', data[6:8])[0] - - if call_id not in self.sessions: - return - - session = self.sessions[call_id] - session.last_seen = asyncio.get_running_loop().time() - - # Handle PPP payload - if protocol == 0x880B: # PPP - await self._handle_ppp(data[8:], session) - - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "pptp_server", f"Error handling GRE packet: {e}") - - async def _handle_ppp(self, data: bytes, session: PPTPSession): - """Handle PPP payload""" - try: - if len(data) < 4: - return - - protocol = struct.unpack('!H', data[2:4])[0] - - if protocol == 0x0021: # IP - ip_packet = data[4:] - await self._handle_ip_packet(ip_packet, session) - - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "pptp_server", f"Error handling PPP packet: {e}") - - async def _handle_ip_packet(self, data: bytes, session: PPTPSession): - """Handle IP packet inside PPP frame""" - try: - # Parse IP header - ip_header = IPParser.parse_ipv4_header(data) - - # Update statistics - session.bytes_in += len(data) - - # Forward packet to destination - if ip_header.protocol == socket.IPPROTO_TCP: - await self._forward_tcp(data, session) - elif ip_header.protocol == socket.IPPROTO_UDP: - await self._forward_udp(data, session) - - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "pptp_server", f"Error handling IP packet: {e}") - - async def _forward_tcp(self, data: bytes, session: PPTPSession): - """Forward TCP packet""" - # This will be handled by the TCP forwarding engine - # Just a placeholder for now - pass - - async def _forward_udp(self, data: bytes, session: PPTPSession): - """Forward UDP packet""" - # This will be handled by the UDP forwarding engine - # Just a placeholder for now - pass diff --git a/core/process_lock.py b/core/process_lock.py deleted file mode 100644 index 0eb65c3e089748e4d22329398fc4461994b44615..0000000000000000000000000000000000000000 --- a/core/process_lock.py +++ /dev/null @@ -1,51 +0,0 @@ -""" -Process Lock Handler for VPN Server -Ensures only one instance of the server is running -""" - -import os -import fcntl -import errno -import atexit -import logging -from typing import Optional - -logger = logging.getLogger(__name__) - -class ProcessLock: - def __init__(self, lock_file: str = "/tmp/outline_vpn.lock"): - self.lock_file = lock_file - self.lock_fd: Optional[int] = None - atexit.register(self.release) - - def acquire(self) -> bool: - """Acquire process lock. Returns True if successful, False if already locked.""" - try: - # Create or open lock file - self.lock_fd = os.open(self.lock_file, os.O_CREAT | os.O_RDWR) - fcntl.flock(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - - # Write PID to lock file - os.truncate(self.lock_fd, 0) - os.write(self.lock_fd, str(os.getpid()).encode()) - - return True - - except (IOError, OSError) as e: - if e.errno == errno.EWOULDBLOCK: - # Another instance is running - logger.warning("Another instance of the VPN server is already running") - else: - logger.error(f"Failed to acquire process lock: {e}") - return False - - def release(self): - """Release the process lock""" - if self.lock_fd is not None: - try: - fcntl.flock(self.lock_fd, fcntl.LOCK_UN) - os.close(self.lock_fd) - os.unlink(self.lock_file) - self.lock_fd = None - except (IOError, OSError) as e: - logger.error(f"Failed to release process lock: {e}") diff --git a/core/services/__pycache__/user_service.cpython-311.pyc b/core/services/__pycache__/user_service.cpython-311.pyc deleted file mode 100644 index fda1b5284fe295de88458bfca1842f00ac4f5d64..0000000000000000000000000000000000000000 Binary files a/core/services/__pycache__/user_service.cpython-311.pyc and /dev/null differ diff --git a/core/services/user_service.py b/core/services/user_service.py deleted file mode 100644 index d67967b9fb365d3b0536fd52055e1112e2c6f811..0000000000000000000000000000000000000000 --- a/core/services/user_service.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -User management and authentication services -""" -from datetime import datetime, timedelta -from typing import Optional, List, Tuple -from sqlalchemy.orm import Session -from sqlalchemy.exc import IntegrityError -from core.models.user import User, UserRole, UserStatus, UserSession, UserPermission - -class UserService: - def __init__(self, db: Session): - self.db = db - - def create_user(self, username: str, password: str, role: UserRole = UserRole.USER) -> Tuple[bool, str, Optional[User]]: - """ - Create a new user - Returns: (success, message, user) - """ - try: - user = User( - username=username, - password_hash=User.hash_password(password), - role=role, - status=UserStatus.ACTIVE - ) - self.db.add(user) - self.db.commit() - self.db.refresh(user) - return True, "User created successfully", user - except IntegrityError: - self.db.rollback() - return False, "Username already exists", None - except Exception as e: - self.db.rollback() - return False, f"Error creating user: {str(e)}", None - - def authenticate_user(self, username: str, password: str) -> Tuple[bool, str, Optional[User]]: - """ - Authenticate user credentials - Returns: (success, message, user) - """ - user = self.db.query(User).filter(User.username == username).first() - - if not user: - return False, "Invalid username or password", None - - if user.is_locked(): - return False, f"Account is locked until {user.lockout_until}", None - - if user.status != UserStatus.ACTIVE: - return False, f"Account is {user.status.value}", None - - if not user.verify_password(password): - user.record_login_attempt(success=False) - self.db.commit() - return False, "Invalid username or password", None - - user.record_login_attempt(success=True) - self.db.commit() - return True, "Authentication successful", user - - def create_session(self, user: User, ip_address: str, device_info: str = None) -> UserSession: - """Create a new session for user""" - session = UserSession( - user_id=user.id, - token=self._generate_session_token(), - ip_address=ip_address, - device_info=device_info, - expires_at=datetime.utcnow() + timedelta(days=1) - ) - self.db.add(session) - self.db.commit() - self.db.refresh(session) - return session - - def validate_session(self, token: str) -> Tuple[bool, str, Optional[UserSession]]: - """Validate session token""" - session = self.db.query(UserSession).filter(UserSession.token == token).first() - - if not session: - return False, "Invalid session", None - - if session.expires_at < datetime.utcnow(): - return False, "Session expired", None - - # Update last active - session.last_active = datetime.utcnow() - self.db.commit() - - return True, "Session valid", session - - def get_user_permissions(self, user_id: int) -> List[UserPermission]: - """Get user permissions""" - return self.db.query(UserPermission).filter(UserPermission.user_id == user_id).all() - - def _generate_session_token(self) -> str: - """Generate a unique session token""" - import secrets - return secrets.token_urlsafe(32) diff --git a/core/session_tracker.py b/core/session_tracker.py deleted file mode 100644 index 4572bf9342b8055ba2a071a70449ba78798d7c56..0000000000000000000000000000000000000000 --- a/core/session_tracker.py +++ /dev/null @@ -1,287 +0,0 @@ -""" -Session Tracker Module - -Manages and tracks all network sessions across the virtual ISP stack: -- Unified session management across all modules -- Session lifecycle tracking -- Performance metrics and analytics -- Session correlation and debugging -""" - -import time -import threading -import uuid -from typing import Dict, List, Optional, Set, Any, Tuple -from dataclasses import dataclass, field -from enum import Enum -import json - -from .tcp_engine import TCPConnection -from .nat_engine import NATSession - - -class SessionType(Enum): - NAT_SESSION = "NAT_SESSION" - TCP_CONNECTION = "TCP_CONNECTION" - SOCKET_CONNECTION = "SOCKET_CONNECTION" - - -class SessionState(Enum): - INITIALIZING = "INITIALIZING" - ACTIVE = "ACTIVE" - IDLE = "IDLE" - CLOSING = "CLOSING" - CLOSED = "CLOSED" - ERROR = "ERROR" - - -@dataclass -class SessionMetrics: - """Session performance metrics""" - bytes_in: int = 0 - bytes_out: int = 0 - packets_in: int = 0 - packets_out: int = 0 - errors: int = 0 - retransmits: int = 0 - rtt_samples: List[float] = field(default_factory=list) - - @property - def total_bytes(self) -> int: - return self.bytes_in + self.bytes_out - - @property - def total_packets(self) -> int: - return self.packets_in + self.packets_out - - @property - def average_rtt(self) -> float: - return sum(self.rtt_samples) / len(self.rtt_samples) if self.rtt_samples else 0.0 - - def update_bytes(self, bytes_in: int = 0, bytes_out: int = 0): - """Update byte counters""" - self.bytes_in += bytes_in - self.bytes_out += bytes_out - - def update_packets(self, packets_in: int = 0, packets_out: int = 0): - """Update packet counters""" - self.packets_in += packets_in - self.packets_out += packets_out - - def add_rtt_sample(self, rtt: float): - """Add RTT sample""" - self.rtt_samples.append(rtt) - # Keep only last 100 samples - if len(self.rtt_samples) > 100: - self.rtt_samples = self.rtt_samples[-100:] - - def to_dict(self) -> Dict: - """Convert to dictionary""" - return { - 'bytes_in': self.bytes_in, - 'bytes_out': self.bytes_out, - 'packets_in': self.packets_in, - 'packets_out': self.packets_out, - 'total_bytes': self.total_bytes, - 'total_packets': self.total_packets, - 'errors': self.errors, - 'retransmits': self.retransmits, - 'average_rtt': self.average_rtt, - 'rtt_samples_count': len(self.rtt_samples) - } - - -@dataclass -class UnifiedSession: - """Unified session representation""" - id: str - type: SessionType - state: SessionState - start_time: float - last_active: float - source_ip: str - source_port: int - dest_ip: str - dest_port: int - metrics: SessionMetrics = field(default_factory=SessionMetrics) - metadata: Dict[str, Any] = field(default_factory=dict) - - @property - def duration(self) -> float: - """Get session duration""" - return time.time() - self.start_time - - @property - def idle_time(self) -> float: - """Get idle time""" - return time.time() - self.last_active - - def update_activity(self): - """Update last activity timestamp""" - self.last_active = time.time() - - def to_dict(self) -> Dict: - """Convert to dictionary""" - return { - 'id': self.id, - 'type': self.type.value, - 'state': self.state.value, - 'start_time': self.start_time, - 'last_active': self.last_active, - 'duration': self.duration, - 'idle_time': self.idle_time, - 'source_ip': self.source_ip, - 'source_port': self.source_port, - 'dest_ip': self.dest_ip, - 'dest_port': self.dest_port, - 'metrics': self.metrics.to_dict(), - 'metadata': self.metadata - } - - -class SessionTracker: - """Tracks all active network sessions""" - - _instance = None - _lock = threading.Lock() - - def __new__(cls): - with cls._lock: - if cls._instance is None: - cls._instance = super().__new__(cls) - cls._instance._initialized = False - return cls._instance - - def __init__(self): - if self._initialized: - return - - self.sessions: Dict[str, UnifiedSession] = {} - self.lock = threading.Lock() - self.cleanup_thread = threading.Thread(target=self._cleanup_loop) - self.cleanup_thread.daemon = True - self.running = True - self.cleanup_thread.start() - - self._initialized = True - - def create_session(self, session_type: SessionType, source_ip: str, - source_port: int, dest_ip: str, dest_port: int, - **kwargs) -> UnifiedSession: - """Create a new session""" - session = UnifiedSession( - id=str(uuid.uuid4()), - type=session_type, - state=SessionState.INITIALIZING, - start_time=time.time(), - last_active=time.time(), - source_ip=source_ip, - source_port=source_port, - dest_ip=dest_ip, - dest_port=dest_port, - metadata=kwargs - ) - - with self.lock: - self.sessions[session.id] = session - - return session - - def get_session(self, session_id: str) -> Optional[UnifiedSession]: - """Get session by ID""" - return self.sessions.get(session_id) - - def update_session(self, session_id: str, state: Optional[SessionState] = None, - metrics_update: Optional[Dict] = None, - metadata_update: Optional[Dict] = None) -> bool: - """Update session state and metrics""" - session = self.get_session(session_id) - if not session: - return False - - with self.lock: - if state: - session.state = state - - if metrics_update: - session.metrics.update_bytes( - metrics_update.get('bytes_in', 0), - metrics_update.get('bytes_out', 0) - ) - session.metrics.update_packets( - metrics_update.get('packets_in', 0), - metrics_update.get('packets_out', 0) - ) - if 'rtt' in metrics_update: - session.metrics.add_rtt_sample(metrics_update['rtt']) - - if metadata_update: - session.metadata.update(metadata_update) - - session.update_activity() - - return True - - def close_session(self, session_id: str): - """Close a session""" - session = self.get_session(session_id) - if session: - with self.lock: - session.state = SessionState.CLOSED - - def get_all_sessions(self) -> List[UnifiedSession]: - """Get all active sessions""" - with self.lock: - return [s for s in self.sessions.values() - if s.state != SessionState.CLOSED] - - def get_sessions_by_type(self, session_type: SessionType) -> List[UnifiedSession]: - """Get sessions by type""" - with self.lock: - return [s for s in self.sessions.values() - if s.type == session_type and s.state != SessionState.CLOSED] - - def get_sessions_by_ip(self, ip_address: str) -> List[UnifiedSession]: - """Get sessions by IP address""" - with self.lock: - return [s for s in self.sessions.values() - if (s.source_ip == ip_address or s.dest_ip == ip_address) - and s.state != SessionState.CLOSED] - - def _cleanup_loop(self): - """Background cleanup loop""" - while self.running: - time.sleep(60) # Run every minute - try: - self._cleanup_sessions() - except Exception as e: - print(f"Error in cleanup loop: {e}") - - def _cleanup_sessions(self): - """Clean up old sessions""" - current_time = time.time() - to_remove = [] - - with self.lock: - for session_id, session in self.sessions.items(): - # Remove closed sessions after 5 minutes - if (session.state == SessionState.CLOSED and - current_time - session.last_active > 300): - to_remove.append(session_id) - # Remove idle sessions after 30 minutes - elif (session.state != SessionState.CLOSED and - current_time - session.last_active > 1800): - session.state = SessionState.CLOSED - to_remove.append(session_id) - - for session_id in to_remove: - del self.sessions[session_id] - - def shutdown(self): - """Shutdown the tracker""" - self.running = False - if self.cleanup_thread.is_alive(): - self.cleanup_thread.join() - - with self.lock: - self.sessions.clear() diff --git a/core/shadowsocks_protocol.py b/core/shadowsocks_protocol.py deleted file mode 100644 index 631ce1fa4a6ec59bd4b00333bfefddd83cabfee1..0000000000000000000000000000000000000000 --- a/core/shadowsocks_protocol.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -Shadowsocks Protocol Implementation -""" - -import os -import asyncio -import hashlib -from typing import Optional, Tuple -from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 -import logging - -logger = logging.getLogger(__name__) - -class ShadowsocksProtocol: - CHUNK_SIZE = 8192 - - def __init__(self, access_key: str): - self.access_key = access_key - self.cipher = self._create_cipher() - self.buffer = bytearray() - - def _create_cipher(self) -> ChaCha20Poly1305: - """Create ChaCha20-Poly1305 cipher""" - key = hashlib.sha256(self.access_key.encode()).digest() - return ChaCha20Poly1305(key) - - async def handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): - """Handle client connection""" - try: - # Read and decrypt initial packet - data = await reader.read(self.CHUNK_SIZE) - if not data: - return - - # Extract target address - decrypted = self._decrypt_packet(data) - target_addr = self._extract_address(decrypted) - if not target_addr: - logger.error("Invalid target address") - return - - # Connect to target - target_reader, target_writer = await asyncio.open_connection( - target_addr[0], target_addr[1] - ) - - # Start bidirectional forwarding - await self._proxy_data(reader, writer, target_reader, target_writer) - - except Exception as e: - logger.error(f"Connection error: {e}") - finally: - writer.close() - await writer.wait_closed() - - async def _proxy_data(self, - client_reader: asyncio.StreamReader, - client_writer: asyncio.StreamWriter, - target_reader: asyncio.StreamReader, - target_writer: asyncio.StreamWriter): - """Handle bidirectional data forwarding""" - async def forward(reader: asyncio.StreamReader, - writer: asyncio.StreamWriter, - encrypt: bool = False): - try: - while True: - data = await reader.read(self.CHUNK_SIZE) - if not data: - break - if encrypt: - data = self._encrypt_packet(data) - writer.write(data) - await writer.drain() - except Exception as e: - logger.error(f"Forward error: {e}") - finally: - writer.close() - await writer.wait_closed() - - await asyncio.gather( - forward(client_reader, target_writer, encrypt=False), - forward(target_reader, client_writer, encrypt=True) - ) - - def _encrypt_packet(self, data: bytes) -> bytes: - """Encrypt a packet""" - nonce = os.urandom(12) - encrypted = self.cipher.encrypt(nonce, data, None) - return nonce + encrypted - - def _decrypt_packet(self, data: bytes) -> bytes: - """Decrypt a packet""" - nonce, ciphertext = data[:12], data[12:] - return self.cipher.decrypt(nonce, ciphertext, None) - - def _extract_address(self, data: bytes) -> Optional[Tuple[str, int]]: - """Extract address from Shadowsocks address header""" - try: - atyp = data[0] # Address type - - if atyp == 1: # IPv4 - addr = '.'.join(str(b) for b in data[1:5]) - port = int.from_bytes(data[5:7], 'big') - payload_start = 7 - elif atyp == 3: # Domain name - length = data[1] - addr = data[2:2+length].decode() - port = int.from_bytes(data[2+length:4+length], 'big') - payload_start = 4 + length - elif atyp == 4: # IPv6 - addr = ':'.join(format(b, '02x') for b in data[1:17]) - port = int.from_bytes(data[17:19], 'big') - payload_start = 19 - else: - return None - - return addr, port - - except Exception as e: - logger.error(f"Error extracting address: {e}") - return None diff --git a/core/socket_translator.py b/core/socket_translator.py deleted file mode 100644 index 845a0520aec396ada3a76f2e8efce6360111fac6..0000000000000000000000000000000000000000 --- a/core/socket_translator.py +++ /dev/null @@ -1,339 +0,0 @@ -""" -Socket Translator Module - -Bridges virtual connections to real host sockets: -- Map virtual connections to host sockets/HTTP clients -- Bidirectional data streaming -- Connection lifecycle management -- Protocol translation (TCP/UDP to host sockets) -""" - -import socket -import threading -import time -import asyncio -import aiohttp -import ssl -from typing import Dict, Optional, Callable, Tuple, Any -from dataclasses import dataclass -from enum import Enum -import urllib.parse -import json - -from .tcp_engine import TCPConnection - - -class ConnectionType(Enum): - TCP_SOCKET = "TCP_SOCKET" - UDP_SOCKET = "UDP_SOCKET" - HTTP_CLIENT = "HTTP_CLIENT" - HTTPS_CLIENT = "HTTPS_CLIENT" - - -@dataclass -class SocketConnection: - """Represents a socket connection""" - connection_id: str - connection_type: ConnectionType - virtual_connection: Optional[TCPConnection] - host_socket: Optional[socket.socket] - remote_host: str - remote_port: int - created_time: float - last_activity: float - bytes_sent: int = 0 - bytes_received: int = 0 - is_connected: bool = False - error_count: int = 0 - - def update_activity(self, bytes_transferred: int = 0, direction: str = 'sent'): - """Update connection activity""" - self.last_activity = time.time() - if direction == 'sent': - self.bytes_sent += bytes_transferred - else: - self.bytes_received += bytes_transferred - - def to_dict(self) -> Dict: - """Convert to dictionary""" - return { - 'connection_id': self.connection_id, - 'connection_type': self.connection_type.value, - 'remote_host': self.remote_host, - 'remote_port': self.remote_port, - 'created_time': self.created_time, - 'last_activity': self.last_activity, - 'bytes_sent': self.bytes_sent, - 'bytes_received': self.bytes_received, - 'is_connected': self.is_connected, - 'error_count': self.error_count, - 'duration': time.time() - self.created_time - } - - -class SocketTranslator: - """Manages socket translations and connections""" - - _instance = None - _lock = threading.Lock() - - def __new__(cls): - with cls._lock: - if cls._instance is None: - cls._instance = super().__new__(cls) - cls._instance._initialized = False - return cls._instance - - def __init__(self): - if self._initialized: - return - - self.connections: Dict[str, SocketConnection] = {} - self.lock = threading.Lock() - self.cleanup_thread = threading.Thread(target=self._cleanup_loop) - self.cleanup_thread.daemon = True - self.running = True - self.cleanup_thread.start() - - # SSL context for HTTPS - self.ssl_context = ssl.create_default_context() - self.ssl_context.check_hostname = False - self.ssl_context.verify_mode = ssl.CERT_NONE - - # Async event loop - self.loop = asyncio.new_event_loop() - self.async_thread = threading.Thread(target=self._run_async_loop) - self.async_thread.daemon = True - self.async_thread.start() - - self._initialized = True - - def create_connection(self, connection_type: ConnectionType, - remote_host: str, remote_port: int, - virtual_connection: Optional[TCPConnection] = None) -> str: - """Create a new connection""" - connection = SocketConnection( - connection_id=str(time.time_ns()), - connection_type=connection_type, - virtual_connection=virtual_connection, - host_socket=None, - remote_host=remote_host, - remote_port=remote_port, - created_time=time.time(), - last_activity=time.time() - ) - - with self.lock: - self.connections[connection.connection_id] = connection - - return connection.connection_id - - async def connect(self, connection_id: str) -> bool: - """Establish connection""" - connection = self.connections.get(connection_id) - if not connection: - return False - - try: - if connection.connection_type in (ConnectionType.TCP_SOCKET, ConnectionType.UDP_SOCKET): - # Create socket - sock_type = socket.SOCK_STREAM if connection.connection_type == ConnectionType.TCP_SOCKET else socket.SOCK_DGRAM - sock = socket.socket(socket.AF_INET, sock_type) - sock.setblocking(False) - - # Connect - try: - sock.connect((connection.remote_host, connection.remote_port)) - except BlockingIOError: - pass # Expected for non-blocking socket - - connection.host_socket = sock - connection.is_connected = True - - # Start monitoring - asyncio.create_task(self._monitor_socket(connection)) - - elif connection.connection_type in (ConnectionType.HTTP_CLIENT, ConnectionType.HTTPS_CLIENT): - # HTTP(S) connection will be made per request - connection.is_connected = True - - connection.update_activity() - return True - - except Exception as e: - print(f"Connection error: {e}") - connection.error_count += 1 - return False - - async def send_data(self, connection_id: str, data: bytes) -> bool: - """Send data through connection""" - connection = self.connections.get(connection_id) - if not connection or not connection.is_connected: - return False - - try: - if connection.connection_type in (ConnectionType.TCP_SOCKET, ConnectionType.UDP_SOCKET): - await self.loop.sock_sendall(connection.host_socket, data) - connection.update_activity(len(data), 'sent') - - elif connection.connection_type in (ConnectionType.HTTP_CLIENT, ConnectionType.HTTPS_CLIENT): - # Parse HTTP request - request = self._parse_http_request(data) - if not request: - return False - - # Make HTTP request - async with aiohttp.ClientSession() as session: - url = f"{'https' if connection.connection_type == ConnectionType.HTTPS_CLIENT else 'http'}://{connection.remote_host}:{connection.remote_port}{request['path']}" - - async with session.request( - method=request['method'], - url=url, - headers=request['headers'], - data=request.get('body', b''), - ssl=self.ssl_context if connection.connection_type == ConnectionType.HTTPS_CLIENT else None - ) as response: - # Forward response to virtual connection - response_data = await response.read() - if connection.virtual_connection: - connection.virtual_connection.send_data(response_data) - - connection.update_activity(len(data), 'sent') - connection.update_activity(len(response_data), 'received') - - return True - - except Exception as e: - print(f"Send error: {e}") - connection.error_count += 1 - return False - - def close_connection(self, connection_id: str): - """Close a connection""" - connection = self.connections.get(connection_id) - if connection: - if connection.host_socket: - try: - connection.host_socket.close() - except: - pass - connection.is_connected = False - connection.update_activity() - - def get_connection(self, connection_id: str) -> Optional[SocketConnection]: - """Get connection by ID""" - return self.connections.get(connection_id) - - def _run_async_loop(self): - """Run async event loop""" - asyncio.set_event_loop(self.loop) - self.loop.run_forever() - - async def _monitor_socket(self, connection: SocketConnection): - """Monitor socket for incoming data""" - while connection.is_connected: - try: - data = await self.loop.sock_recv(connection.host_socket, 8192) - if not data: - break - - connection.update_activity(len(data), 'received') - - # Forward data to virtual connection - if connection.virtual_connection: - connection.virtual_connection.send_data(data) - - except Exception as e: - print(f"Monitor error: {e}") - connection.error_count += 1 - break - - connection.is_connected = False - - def _cleanup_loop(self): - """Background cleanup loop""" - while self.running: - time.sleep(60) # Run every minute - try: - self._cleanup_connections() - except Exception as e: - print(f"Cleanup error: {e}") - - def _cleanup_connections(self): - """Clean up inactive connections""" - current_time = time.time() - to_remove = [] - - with self.lock: - for connection_id, connection in self.connections.items(): - # Remove if: - # 1. Not connected and inactive for 5 minutes - # 2. Connected but inactive for 30 minutes - # 3. Too many errors - if ((not connection.is_connected and current_time - connection.last_activity > 300) or - (connection.is_connected and current_time - connection.last_activity > 1800) or - connection.error_count > 5): - self.close_connection(connection_id) - to_remove.append(connection_id) - - for connection_id in to_remove: - del self.connections[connection_id] - - def _parse_http_request(self, data: bytes) -> Optional[Dict]: - """Parse HTTP request from raw data""" - try: - # Split into lines - lines = data.decode('utf-8', errors='ignore').split('\r\n') - if not lines: - return None - - # Parse request line - request_line = lines[0].split(' ') - if len(request_line) < 3: - return None - - method, path, version = request_line[0], request_line[1], request_line[2] - - # Parse headers - headers = {} - i = 1 - while i < len(lines): - line = lines[i].strip() - if not line: - break - if ':' in line: - key, value = line.split(':', 1) - headers[key.strip()] = value.strip() - i += 1 - - # Get body - body = '\r\n'.join(lines[i+1:]).encode('utf-8') if i+1 < len(lines) else b'' - - return { - 'method': method, - 'path': path, - 'version': version, - 'headers': headers, - 'body': body - } - - except Exception as e: - print(f"HTTP parse error: {e}") - return None - - def shutdown(self): - """Shutdown the translator""" - self.running = False - if self.cleanup_thread.is_alive(): - self.cleanup_thread.join() - - # Close all connections - with self.lock: - for connection_id in list(self.connections.keys()): - self.close_connection(connection_id) - self.connections.clear() - - # Stop async loop - self.loop.stop() - if self.async_thread.is_alive(): - self.async_thread.join() diff --git a/core/tcp_engine.py b/core/tcp_engine.py deleted file mode 100644 index ea2faf34dd8656e4cf77a531aac110fd4814a412..0000000000000000000000000000000000000000 --- a/core/tcp_engine.py +++ /dev/null @@ -1,356 +0,0 @@ -""" -TCP Engine Module - -Implements a complete TCP state machine in user-space: -- Full TCP state machine (SYN, SYN-ACK, ESTABLISHED, FIN, RST) -- Sequence and acknowledgment number tracking -- Sliding window implementation -- Retransmission and timeout handling -- Congestion control -""" - -import time -import threading -import random -from typing import Dict, List, Optional, Tuple, Callable -from dataclasses import dataclass, field -from enum import Enum -from collections import deque - -from .ip_parser import TCPHeader, IPv4Header, IPParser - - -class TCPState(Enum): - CLOSED = "CLOSED" - LISTEN = "LISTEN" - SYN_SENT = "SYN_SENT" - SYN_RECEIVED = "SYN_RECEIVED" - ESTABLISHED = "ESTABLISHED" - FIN_WAIT_1 = "FIN_WAIT_1" - FIN_WAIT_2 = "FIN_WAIT_2" - CLOSE_WAIT = "CLOSE_WAIT" - CLOSING = "CLOSING" - LAST_ACK = "LAST_ACK" - TIME_WAIT = "TIME_WAIT" - - -@dataclass -class TCPSegment: - """Represents a TCP segment""" - seq_num: int - ack_num: int - flags: int - window: int - data: bytes - timestamp: float = field(default_factory=time.time) - retransmit_count: int = 0 - - @property - def data_length(self) -> int: - """Get data length""" - return len(self.data) - - @property - def seq_end(self) -> int: - """Get sequence number after this segment""" - length = self.data_length - # SYN and FIN consume one sequence number - if self.flags & 0x02: # SYN - length += 1 - if self.flags & 0x01: # FIN - length += 1 - return self.seq_num + length - - -@dataclass -class TCPConnection: - """Represents a TCP connection state""" - # Connection identification - local_ip: str - local_port: int - remote_ip: str - remote_port: int - - # State - state: TCPState = TCPState.CLOSED - - # Sequence numbers - local_seq: int = field(default_factory=lambda: random.randint(0, 0xFFFFFFFF)) - local_ack: int = 0 - remote_seq: int = 0 - remote_ack: int = 0 - initial_seq: int = 0 - - # Window management - local_window: int = 65535 - remote_window: int = 65535 - window_scale: int = 0 - - # Buffers - send_buffer: deque = field(default_factory=deque) - recv_buffer: deque = field(default_factory=deque) - out_of_order_buffer: Dict[int, bytes] = field(default_factory=dict) - - # Retransmission - unacked_segments: Dict[int, TCPSegment] = field(default_factory=dict) - retransmit_timer: Optional[float] = None - rto: float = 1.0 # Retransmission timeout - srtt: float = 0.0 # Smoothed round-trip time - rttvar: float = 0.0 # Round-trip time variation - - # Congestion control - cwnd: int = 1 # Congestion window (in MSS) - ssthresh: int = 65535 # Slow start threshold - dupacks: int = 0 # Duplicate ACK count - mss: int = 1460 # Maximum segment size - - # Callbacks - on_data_received: Optional[Callable[[bytes], None]] = None - on_state_change: Optional[Callable[[TCPState], None]] = None - - def __post_init__(self): - self.initial_seq = self.local_seq - - def handle_packet(self, packet: bytes): - """Process incoming TCP packet""" - try: - # Parse headers - ip_header, payload = IPParser.parse_ipv4_header(packet) - tcp_header, data = IPParser.parse_tcp_header(payload) - - # Process based on current state - if self.state == TCPState.LISTEN: - self._handle_listen(tcp_header, data) - elif self.state == TCPState.SYN_SENT: - self._handle_syn_sent(tcp_header, data) - elif self.state == TCPState.SYN_RECEIVED: - self._handle_syn_received(tcp_header, data) - elif self.state == TCPState.ESTABLISHED: - self._handle_established(tcp_header, data) - elif self.state in (TCPState.FIN_WAIT_1, TCPState.FIN_WAIT_2): - self._handle_fin_wait(tcp_header, data) - elif self.state == TCPState.CLOSE_WAIT: - self._handle_close_wait(tcp_header, data) - elif self.state == TCPState.LAST_ACK: - self._handle_last_ack(tcp_header, data) - - # Update RTT if this is an ACK for a sent packet - if tcp_header.ack and tcp_header.ack_num > self.local_seq: - self._update_rtt(tcp_header.ack_num) - - # Handle retransmission timer - self._manage_retransmission_timer() - - except Exception as e: - print(f"Error handling packet: {e}") - - def send_data(self, data: bytes): - """Send data over the connection""" - if self.state != TCPState.ESTABLISHED: - return False - - # Add to send buffer - self.send_buffer.extend(data) - - # Try to send what we can - self._send_from_buffer() - - return True - - def close(self): - """Initiate connection close""" - if self.state == TCPState.ESTABLISHED: - self._send_fin() - self._set_state(TCPState.FIN_WAIT_1) - elif self.state == TCPState.CLOSE_WAIT: - self._send_fin() - self._set_state(TCPState.LAST_ACK) - - def _set_state(self, new_state: TCPState): - """Change connection state""" - if new_state != self.state: - self.state = new_state - if self.on_state_change: - self.on_state_change(new_state) - - def _send_packet(self, flags: int, data: bytes = b''): - """Send TCP packet""" - segment = TCPSegment( - seq_num=self.local_seq, - ack_num=self.local_ack, - flags=flags, - window=self.local_window, - data=data - ) - - # Add to unacked segments if not pure ACK - if data or flags != 0x10: # Not pure ACK - self.unacked_segments[self.local_seq] = segment - - # Update sequence number - self.local_seq = (self.local_seq + len(data)) % 0x100000000 - if flags & 0x02: # SYN - self.local_seq = (self.local_seq + 1) % 0x100000000 - if flags & 0x01: # FIN - self.local_seq = (self.local_seq + 1) % 0x100000000 - - # TODO: Actually send the packet - - def _handle_listen(self, header: TCPHeader, data: bytes): - """Handle LISTEN state""" - if header.syn: - self.remote_seq = header.seq_num - self.local_ack = (header.seq_num + 1) % 0x100000000 - self._send_packet(0x12) # SYN-ACK - self._set_state(TCPState.SYN_RECEIVED) - - def _handle_syn_sent(self, header: TCPHeader, data: bytes): - """Handle SYN_SENT state""" - if header.syn and header.ack: - if header.ack_num == (self.initial_seq + 1) % 0x100000000: - self.remote_seq = header.seq_num - self.local_ack = (header.seq_num + 1) % 0x100000000 - self._send_packet(0x10) # ACK - self._set_state(TCPState.ESTABLISHED) - - def _handle_established(self, header: TCPHeader, data: bytes): - """Handle ESTABLISHED state""" - if data: - if header.seq_num == self.local_ack: - # In-order segment - if self.on_data_received: - self.on_data_received(data) - self.local_ack = (self.local_ack + len(data)) % 0x100000000 - self._send_packet(0x10) # ACK - elif header.seq_num > self.local_ack: - # Out-of-order segment - self.out_of_order_buffer[header.seq_num] = data - self._send_packet(0x10) # ACK - else: - # Duplicate segment - self._send_packet(0x10) # ACK - - if header.ack: - # Process acknowledgments - self._handle_ack(header.ack_num) - - if header.fin: - self.local_ack = (self.local_ack + 1) % 0x100000000 - self._send_packet(0x10) # ACK - self._set_state(TCPState.CLOSE_WAIT) - - def _handle_ack(self, ack_num: int): - """Handle incoming acknowledgment""" - # Remove acknowledged segments - acknowledged = [seq for seq in self.unacked_segments.keys() - if seq < ack_num] - for seq in acknowledged: - del self.unacked_segments[seq] - - # Update congestion window - if self.cwnd < self.ssthresh: - # Slow start - self.cwnd += 1 - else: - # Congestion avoidance - self.cwnd += 1 / self.cwnd - - # Try to send more data - self._send_from_buffer() - - def _send_from_buffer(self): - """Send data from send buffer""" - while self.send_buffer: - # Calculate how much we can send - window = min(self.remote_window, self.cwnd * self.mss) - if not window: - break - - # Get data to send - data = bytes(list(self.send_buffer)[:window]) - if not data: - break - - # Remove from buffer and send - for _ in range(len(data)): - self.send_buffer.popleft() - self._send_packet(0x18, data) # PSH-ACK - - def _update_rtt(self, ack_num: int): - """Update RTT estimation""" - for seq, segment in self.unacked_segments.items(): - if seq == ack_num - 1: - rtt = time.time() - segment.timestamp - if self.srtt == 0: - self.srtt = rtt - self.rttvar = rtt / 2 - else: - self.rttvar = (0.75 * self.rttvar + - 0.25 * abs(self.srtt - rtt)) - self.srtt = 0.875 * self.srtt + 0.125 * rtt - self.rto = self.srtt + max(4 * self.rttvar, 0.5) - break - - def _manage_retransmission_timer(self): - """Manage retransmission timer""" - if not self.unacked_segments: - self.retransmit_timer = None - return - - current_time = time.time() - if self.retransmit_timer is None: - self.retransmit_timer = current_time + self.rto - elif current_time >= self.retransmit_timer: - # Timeout occurred - self._handle_timeout() - - def _handle_timeout(self): - """Handle retransmission timeout""" - # Exponential backoff - self.rto *= 2 - - # Reset congestion window - self.ssthresh = max(2, self.cwnd // 2) - self.cwnd = 1 - - # Retransmit oldest unacked segment - if self.unacked_segments: - oldest_seq = min(self.unacked_segments.keys()) - segment = self.unacked_segments[oldest_seq] - if segment.retransmit_count < 5: - segment.retransmit_count += 1 - self._send_packet(segment.flags, segment.data) - else: - # Too many retransmissions, close connection - self._set_state(TCPState.CLOSED) - - # Reset timer - self.retransmit_timer = time.time() + self.rto - - def _send_fin(self): - """Send FIN packet""" - self._send_packet(0x11) # FIN-ACK - - def _handle_fin_wait(self, header: TCPHeader, data: bytes): - """Handle FIN_WAIT states""" - if self.state == TCPState.FIN_WAIT_1: - if header.ack and header.ack_num == self.local_seq: - self._set_state(TCPState.FIN_WAIT_2) - - if header.fin: - self.local_ack = (header.seq_num + 1) % 0x100000000 - self._send_packet(0x10) # ACK - if self.state == TCPState.FIN_WAIT_1: - self._set_state(TCPState.CLOSING) - else: # FIN_WAIT_2 - self._set_state(TCPState.TIME_WAIT) - - def _handle_close_wait(self, header: TCPHeader, data: bytes): - """Handle CLOSE_WAIT state""" - if header.ack: - self._handle_ack(header.ack_num) - - def _handle_last_ack(self, header: TCPHeader, data: bytes): - """Handle LAST_ACK state""" - if header.ack and header.ack_num == self.local_seq: - self._set_state(TCPState.CLOSED) diff --git a/core/tcp_forward.py b/core/tcp_forward.py deleted file mode 100644 index 3cbc5d2599d2b97d14f3887bd19af36fd0622c14..0000000000000000000000000000000000000000 --- a/core/tcp_forward.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -TCP Forwarding Engine Module for Outline VPN - -Handles TCP traffic forwarding and connection tracking with Shadowsocks protocol support -""" - -import asyncio -import logging -import socket -from typing import Dict, Set, Optional, Tuple -from dataclasses import dataclass -from datetime import datetime -from .shadowsocks_protocol import ShadowsocksProtocol - -logger = logging.getLogger(__name__) - -@dataclass -class OutlineTCPConnection: - client_addr: Tuple[str, int] - target_addr: Tuple[str, int] - client_writer: asyncio.StreamWriter - target_writer: asyncio.StreamWriter - shadowsocks: ShadowsocksProtocol - bytes_in: int = 0 - bytes_out: int = 0 - created_at: datetime = datetime.now() - last_activity: datetime = datetime.now() - -class OutlineTCPForwardingEngine: - def __init__(self, access_key: str): - self.connections: Dict[str, OutlineTCPConnection] = {} - self.active_ports: Set[int] = set() - self._lock = asyncio.Lock() - self.buffer_size = 8192 - self.shadowsocks = ShadowsocksProtocol(access_key) - - async def create_connection(self, - client_reader: asyncio.StreamReader, - client_writer: asyncio.StreamWriter, - target_host: str, - target_port: int) -> Optional[OutlineTCPConnection]: - """Create a new TCP connection with Shadowsocks encryption""" - try: - # Get client information - client_addr = client_writer.get_extra_info('peername') - - # Connect to target - target_reader, target_writer = await asyncio.open_connection( - target_host, target_port - ) - - # Create connection object - conn = OutlineTCPConnection( - client_addr=client_addr, - target_addr=(target_host, target_port), - client_writer=client_writer, - target_writer=target_writer, - shadowsocks=self.shadowsocks - ) - - # Store connection - conn_id = f"{client_addr[0]}:{client_addr[1]}-{target_host}:{target_port}" - async with self._lock: - self.connections[conn_id] = conn - - # Start bidirectional forwarding with encryption - asyncio.create_task(self._forward_stream( - client_reader, target_writer, conn, 'in')) - asyncio.create_task(self._forward_stream( - target_reader, client_writer, conn, 'out')) - - logger.info(f"Created Outline TCP connection: {conn_id}") - return conn - - except Exception as e: - logger.error(f"Error creating Outline TCP connection: {e}") - if client_writer: - client_writer.close() - await client_writer.wait_closed() - return None - - async def _forward_stream(self, - reader: asyncio.StreamReader, - writer: asyncio.StreamWriter, - conn: OutlineTCPConnection, - direction: str): - """Forward data with Shadowsocks encryption/decryption""" - try: - while True: - data = await reader.read(self.buffer_size) - if not data: - break - - # Handle encryption/decryption - if direction == 'in': - # Decrypt incoming data from client - data = conn.shadowsocks._decrypt_packet(data) - conn.bytes_in += len(data) - else: - # Encrypt outgoing data to client - data = conn.shadowsocks._encrypt_packet(data) - conn.bytes_out += len(data) - - writer.write(data) - await writer.drain() - conn.last_activity = datetime.now() - - except Exception as e: - logger.error(f"Error forwarding Outline data: {e}") - - finally: - writer.close() - await writer.wait_closed() - # Clean up connection - conn_id = (f"{conn.client_addr[0]}:{conn.client_addr[1]}-" - f"{conn.target_addr[0]}:{conn.target_addr[1]}") - async with self._lock: - if conn_id in self.connections: - del self.connections[conn_id] - - async def cleanup_inactive(self, timeout: int = 300): - """Clean up inactive connections""" - while True: - try: - now = datetime.now() - to_remove = [] - - async with self._lock: - for conn_id, conn in self.connections.items(): - if (now - conn.last_activity).seconds > timeout: - to_remove.append(conn_id) - - for conn_id in to_remove: - if conn_id in self.connections: - conn = self.connections[conn_id] - conn.client_writer.close() - conn.target_writer.close() - del self.connections[conn_id] - logger.info(f"Cleaned up inactive connection: {conn_id}") - - await asyncio.sleep(60) # Check every minute - - except Exception as e: - logger.error(f"Error in connection cleanup: {e}") - await asyncio.sleep(60) # Retry after error - - def get_connection_stats(self) -> Dict[str, Dict]: - """Get statistics for all active connections""" - stats = {} - for conn_id, conn in self.connections.items(): - stats[conn_id] = { - 'bytes_in': conn.bytes_in, - 'bytes_out': conn.bytes_out, - 'created_at': conn.created_at.isoformat(), - 'last_activity': conn.last_activity.isoformat(), - 'client_addr': f"{conn.client_addr[0]}:{conn.client_addr[1]}", - 'target_addr': f"{conn.target_addr[0]}:{conn.target_addr[1]}" - } - return stats diff --git a/core/traffic_forwarder.py b/core/traffic_forwarder.py deleted file mode 100644 index 9ee4f183afc160f52b5758dac2de073f3b91d60e..0000000000000000000000000000000000000000 --- a/core/traffic_forwarder.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -Traffic Forwarding Engine -Handles IP packet forwarding and NAT for VPN tunnels -""" - -import asyncio -import socket -import struct -from typing import Dict, Optional, Tuple, Union -from dataclasses import dataclass -import os -from .ip_parser import IPv4Header, IPParser -from .logger import Logger, LogCategory -from .nat_engine import NATEngine - -@dataclass -class ForwardSession: - src_ip: str - dst_ip: str - src_port: int - dst_port: int - protocol: int - created_at: float - last_seen: float - bytes_in: int = 0 - bytes_out: int = 0 - -class TrafficForwarder: - """Handles packet forwarding and NAT""" - - def __init__(self, logger: Logger, nat_engine: NATEngine): - self.logger = logger - self.nat_engine = nat_engine - self.sessions: Dict[Tuple[str, str, int, int, int], ForwardSession] = {} - self.tcp_connections = {} - self.udp_endpoints = {} - - async def forward_packet(self, data: bytes, client_ip: str) -> Optional[bytes]: - """Forward an IP packet""" - try: - # Parse IP header - ip_header = IPParser.parse_ipv4_header(data) - - # Apply NAT - translated_packet = self.nat_engine.translate_outbound(data) - if not translated_packet: - return None - - # Track session - session_key = ( - ip_header.src_ip, - ip_header.dst_ip, - ip_header.protocol, - self._get_src_port(data[ip_header.ihl*4:], ip_header.protocol), - self._get_dst_port(data[ip_header.ihl*4:], ip_header.protocol) - ) - - if session_key not in self.sessions: - self.sessions[session_key] = ForwardSession( - src_ip=ip_header.src_ip, - dst_ip=ip_header.dst_ip, - src_port=session_key[3], - dst_port=session_key[4], - protocol=ip_header.protocol, - created_at=asyncio.get_running_loop().time(), - last_seen=asyncio.get_running_loop().time() - ) - - session = self.sessions[session_key] - session.last_seen = asyncio.get_running_loop().time() - session.bytes_out += len(data) - - # Forward based on protocol - if ip_header.protocol == socket.IPPROTO_TCP: - return await self._forward_tcp(translated_packet, session) - elif ip_header.protocol == socket.IPPROTO_UDP: - return await self._forward_udp(translated_packet, session) - else: - # Forward other IP protocols directly - return translated_packet - - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "traffic_forwarder", f"Error forwarding packet: {e}") - return None - - async def _forward_tcp(self, data: bytes, session: ForwardSession) -> Optional[bytes]: - """Forward TCP packet""" - try: - ip_header = IPParser.parse_ipv4_header(data) - tcp_header_offset = ip_header.ihl * 4 - - if len(data) < tcp_header_offset + 20: # TCP header is at least 20 bytes - return None - - # Parse TCP header - tcp_header = data[tcp_header_offset:tcp_header_offset + 20] - flags = tcp_header[13] - seq_num = struct.unpack('!I', tcp_header[4:8])[0] - ack_num = struct.unpack('!I', tcp_header[8:12])[0] - - conn_key = (session.src_ip, session.src_port, session.dst_ip, session.dst_port) - - # Handle TCP state - if flags & 0x02: # SYN - if conn_key not in self.tcp_connections: - self.tcp_connections[conn_key] = { - 'state': 'SYN_SENT', - 'seq': seq_num, - 'ack': 0 - } - elif flags & 0x01: # FIN - if conn_key in self.tcp_connections: - self.tcp_connections[conn_key]['state'] = 'FIN_WAIT' - elif flags & 0x04: # RST - if conn_key in self.tcp_connections: - del self.tcp_connections[conn_key] - - # Forward the packet - return await self._send_packet(data) - - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "traffic_forwarder", f"Error forwarding TCP: {e}") - return None - - async def _forward_udp(self, data: bytes, session: ForwardSession) -> Optional[bytes]: - """Forward UDP packet""" - try: - ip_header = IPParser.parse_ipv4_header(data) - udp_header_offset = ip_header.ihl * 4 - - if len(data) < udp_header_offset + 8: # UDP header is 8 bytes - return None - - # Track UDP endpoint - endpoint_key = (session.src_ip, session.src_port, session.dst_ip, session.dst_port) - self.udp_endpoints[endpoint_key] = asyncio.get_running_loop().time() - - # Forward the packet - return await self._send_packet(data) - - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "traffic_forwarder", f"Error forwarding UDP: {e}") - return None - - async def _send_packet(self, data: bytes) -> Optional[bytes]: - """Send packet to destination""" - try: - # This is where you'd actually send the packet - # For now, we'll just return it for the VPN server to handle - return data - - except Exception as e: - self.logger.error(LogCategory.SYSTEM, "traffic_forwarder", f"Error sending packet: {e}") - return None - - def _get_src_port(self, transport_header: bytes, protocol: int) -> int: - """Extract source port from transport header""" - if len(transport_header) >= 2: - return struct.unpack('!H', transport_header[0:2])[0] - return 0 - - def _get_dst_port(self, transport_header: bytes, protocol: int) -> int: - """Extract destination port from transport header""" - if len(transport_header) >= 4: - return struct.unpack('!H', transport_header[2:4])[0] - return 0 - - async def cleanup(self): - """Clean up expired sessions""" - current_time = asyncio.get_running_loop().time() - - # Clean TCP connections - for key, conn in list(self.tcp_connections.items()): - if current_time - conn.get('last_seen', 0) > 300: # 5 minutes timeout - del self.tcp_connections[key] - - # Clean UDP endpoints - for key, last_seen in list(self.udp_endpoints.items()): - if current_time - last_seen > 60: # 1 minute timeout - del self.udp_endpoints[key] - - # Clean sessions - for key, session in list(self.sessions.items()): - if current_time - session.last_seen > 300: # 5 minutes timeout - del self.sessions[key] diff --git a/core/traffic_router.py b/core/traffic_router.py deleted file mode 100644 index e3877442d7bd67e32e40adcd0a8b1b7a3f740887..0000000000000000000000000000000000000000 --- a/core/traffic_router.py +++ /dev/null @@ -1,221 +0,0 @@ -""" -Traffic Router Module -Handles routing of all client traffic with bandwidth monitoring -""" - -import asyncio -import socket -import logging -import ipaddress -from typing import Dict, Any, Optional, Tuple -from datetime import datetime - -from .tcp_forward import OutlineTCPForwardingEngine as TCPForwardingEngine -from .nat_engine import NATEngine - -logger = logging.getLogger(__name__) - -class BandwidthMonitor: - def __init__(self): - self.total_bytes_in = 0 - self.total_bytes_out = 0 - self.user_bandwidth: Dict[str, Dict[str, int]] = {} - self.start_time = datetime.now() - - def update(self, user_id: str, bytes_in: int = 0, bytes_out: int = 0): - """Update bandwidth usage for a user""" - if user_id not in self.user_bandwidth: - self.user_bandwidth[user_id] = { - "bytes_in": 0, - "bytes_out": 0, - "last_update": datetime.now() - } - - self.user_bandwidth[user_id]["bytes_in"] += bytes_in - self.user_bandwidth[user_id]["bytes_out"] += bytes_out - self.user_bandwidth[user_id]["last_update"] = datetime.now() - - self.total_bytes_in += bytes_in - self.total_bytes_out += bytes_out - - def get_stats(self) -> Dict: - """Get bandwidth statistics""" - current_time = datetime.now() - uptime = (current_time - self.start_time).total_seconds() - - return { - "total_bytes_in": self.total_bytes_in, - "total_bytes_out": self.total_bytes_out, - "avg_speed_in": self.total_bytes_in / uptime if uptime > 0 else 0, - "avg_speed_out": self.total_bytes_out / uptime if uptime > 0 else 0, - "user_stats": self.user_bandwidth - } - -class TrafficRouter: - """Manages traffic routing for VPN clients with bandwidth monitoring""" - - def __init__(self, config: Dict[str, Any], logger_instance=None): - self.config = config - self.is_running = False - - # VPN server configuration - self.vpn_host = self.config.get("vpn_host", "0.0.0.0") - self.vpn_port = self.config.get("vpn_port", 9000) - - # Virtual network configuration - self.virtual_network = ipaddress.ip_network( - self.config.get("virtual_network", "10.0.0.0/24") - ) - self.virtual_gateway = str(next(self.virtual_network.hosts())) - - # Initialize engines - self.nat_engine = NATEngine() - self.tcp_engine = TCPForwardingEngine(access_key="") - self.bandwidth_monitor = BandwidthMonitor() - self.logger = logger_instance if logger_instance else logging.getLogger(__name__) - - # Server instances - self.loop = None - self.vpn_server = None - - # Statistics - self.stats = { - "total_connections": 0, - "active_connections": 0, - "bytes_forwarded": 0, - "nat_sessions": 0, - "errors": 0 - } - - async def start(self): - """Start the traffic router""" - if self.is_running: - logger.warning("Traffic Router is already running") - return True - - self.is_running = True - self.loop = asyncio.get_event_loop() - - try: - # Start VPN server - self.vpn_server = await asyncio.start_server( - self._handle_client_connection, - self.vpn_host, - self.vpn_port - ) - - self.logger.info(f"Traffic Router started on {self.vpn_host}:{self.vpn_port}") - self.logger.info(f"Virtual network: {self.virtual_network}") - self.logger.info(f"Virtual gateway: {self.virtual_gateway}") - - # Start NAT engine - await self.nat_engine.start() - - return True - - except Exception as e: - logger.error(f"Failed to start Traffic Router: {e}") - self.is_running = False - return False - - async def stop(self): - """Stop the traffic router""" - if not self.is_running: - return - - self.is_running = False - - # Stop NAT engine - await self.nat_engine.stop() - - # Close VPN server - if self.vpn_server: - self.vpn_server.close() - await self.vpn_server.wait_closed() - - logger.info("Traffic Router stopped") - - async def _handle_client_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): - """Handle incoming client connection""" - peer = writer.get_extra_info('peername') - logger.info(f"New client connection from {peer}") - - try: - buffer_size = self.config.get("buffer_size", 8192) - user_id = self.config.get("user_id", "unknown") - - while self.is_running: - data = await reader.read(buffer_size) - if not data: - break - - # Forward data and track bandwidth - bytes_forwarded = await self._forward_data(data, "client", reader, writer) - self.bandwidth_monitor.update(user_id, bytes_in=bytes_forwarded) - self.stats["bytes_forwarded"] += bytes_forwarded - - except Exception as e: - logger.error(f"Error handling client {peer}: {e}") - self.stats["errors"] += 1 - finally: - writer.close() - await writer.wait_closed() - - async def _forward_data(self, data: bytes, source: str, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> int: - """Forward data and return bytes forwarded""" - try: - if source == "client": - target_addr = self._extract_target_address(data) - if not target_addr: - return 0 - - # Create NAT session - session = self.nat_engine.create_session( - virtual_ip=writer.get_extra_info('peername')[0], - virtual_port=writer.get_extra_info('peername')[1], - real_ip=target_addr[0], - real_port=target_addr[1] - ) - - # Forward through TCP engine - conn = await self.tcp_engine.create_connection( - reader, writer, - target_addr[0], target_addr[1] - ) - - if conn: - return len(data) - return 0 - - except Exception as e: - logger.error(f"Error forwarding data: {e}") - return 0 - - def _extract_target_address(self, data: bytes) -> Optional[Tuple[str, int]]: - """Extract target address from packet""" - try: - if len(data) < 7: - return None - - addr_type = data[0] - if addr_type == 1: # IPv4 - ip = socket.inet_ntoa(data[1:5]) - port = int.from_bytes(data[5:7], 'big') - return (ip, port) - elif addr_type == 3: # Domain - domain_len = data[1] - domain = data[2:2+domain_len].decode() - port = int.from_bytes(data[2+domain_len:4+domain_len], 'big') - return (domain, port) - return None - except Exception as e: - logger.error(f"Error extracting target address: {e}") - return None - - def get_stats(self) -> Dict: - """Get traffic router statistics""" - return { - "router_stats": self.stats, - "bandwidth_stats": self.bandwidth_monitor.get_stats(), - "nat_stats": self.nat_engine.get_stats() - } diff --git a/core/vpn_auth.py b/core/vpn_auth.py deleted file mode 100644 index 65f54f5ac4da46868ab78cd78cfd6a595d897750..0000000000000000000000000000000000000000 --- a/core/vpn_auth.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -VPN Authentication Manager -Handles authentication across different VPN protocols -""" -from typing import Optional, Tuple -from sqlalchemy.orm import Session -from .database import SessionLocal -from .services.user_service import UserService -from .models.user import User, UserStatus - -class VPNAuthManager: - def __init__(self): - self._db = SessionLocal() - self._user_service = UserService(self._db) - - async def authenticate(self, username: str, password: str, protocol: str, - ip_address: str, device_info: str = None) -> Tuple[bool, str, Optional[str]]: - """ - Authenticate user for VPN access - Returns: (success, message, session_token) - """ - # Authenticate user - success, message, user = self._user_service.authenticate_user(username, password) - if not success: - return False, message, None - - # Create session if authentication successful - session = self._user_service.create_session(user, ip_address, device_info) - - return True, "Authentication successful", session.token - - async def validate_session(self, token: str) -> Tuple[bool, str, Optional[User]]: - """ - Validate a session token - Returns: (success, message, user) - """ - success, message, session = self._user_service.validate_session(token) - if not success: - return False, message, None - - # Get user - user = self._db.query(User).filter(User.id == session.user_id).first() - if not user or user.status != UserStatus.ACTIVE: - return False, "User inactive or not found", None - - return True, "Session valid", user - - def close(self): - """Close database connection""" - self._db.close() diff --git a/data/vpn.db b/data/vpn.db deleted file mode 100644 index 04a7f7c011065994fc5dc478fcbc4097755cfcf6..0000000000000000000000000000000000000000 Binary files a/data/vpn.db and /dev/null differ diff --git a/logs/outline.log b/logs/outline.log deleted file mode 100644 index f3ea55158f146da4bdacae0e45284f8d11b335e1..0000000000000000000000000000000000000000 --- a/logs/outline.log +++ /dev/null @@ -1,134 +0,0 @@ -2025-08-18 16:27:11,670 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:27:18,846 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:28:04,348 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:28:04,455 [INFO] [SYSTEM] IKEv2 server initialized -2025-08-18 16:28:19,222 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:28:19,313 [INFO] [SYSTEM] IKEv2 server initialized -2025-08-18 16:28:19,421 [INFO] Database initialized successfully -2025-08-18 16:28:32,273 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:28:32,361 [INFO] [SYSTEM] IKEv2 server initialized -2025-08-18 16:28:32,457 [INFO] Database initialized successfully -2025-08-18 16:28:32,459 [INFO] [SYSTEM] VPN server initialized and started on 44.200.228.79 -2025-08-18 16:28:32,462 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:28:32,554 [INFO] NAT rules set up for interface eth0 -2025-08-18 16:28:32,554 [INFO] Internet sharing enabled on interface eth0 -2025-08-18 16:28:32,554 [INFO] NAT engine started -2025-08-18 16:28:32,554 [ERROR] Failed to start Traffic Router: [Errno 99] error while attempting to bind on address ('44.200.228.79', 8388): cannot assign requested address -2025-08-18 16:28:32,554 [INFO] Starting IKEv2 service... -2025-08-18 16:28:32,554 [INFO] Outline VPN server started successfully on port 8388 -2025-08-18 16:30:03,889 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:30:03,976 [INFO] [SYSTEM] IKEv2 server initialized -2025-08-18 16:30:04,085 [INFO] Database initialized successfully -2025-08-18 16:30:04,088 [INFO] [SYSTEM] VPN server initialized and started on 44.200.228.79 -2025-08-18 16:30:04,092 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:30:04,174 [INFO] NAT rules set up for interface eth0 -2025-08-18 16:30:04,174 [INFO] Internet sharing enabled on interface eth0 -2025-08-18 16:30:04,174 [INFO] NAT engine started -2025-08-18 16:30:04,174 [ERROR] Failed to start Traffic Router: [Errno 99] error while attempting to bind on address ('44.200.228.79', 8388): cannot assign requested address -2025-08-18 16:30:04,174 [INFO] Starting IKEv2 service... -2025-08-18 16:30:04,174 [INFO] Outline VPN server started successfully on port 8388 -2025-08-18 16:30:04,459 [INFO] [SYSTEM] IKEv2 server initialized -2025-08-18 16:30:04,576 [INFO] Database initialized successfully -2025-08-18 16:30:04,577 [INFO] [SYSTEM] VPN server initialized and started on 44.200.228.79 -2025-08-18 16:30:04,608 [INFO] WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5000 -2025-08-18 16:30:04,609 [INFO] Press CTRL+C to quit -2025-08-18 16:30:04,610 [INFO] * Restarting with stat -2025-08-18 16:30:04,692 [INFO] NAT rules set up for interface eth0 -2025-08-18 16:30:04,692 [INFO] Internet sharing enabled on interface eth0 -2025-08-18 16:30:04,692 [INFO] NAT engine started -2025-08-18 16:30:04,693 [ERROR] Failed to start Traffic Router: [Errno 99] error while attempting to bind on address ('44.200.228.79', 8388): cannot assign requested address -2025-08-18 16:30:04,693 [INFO] Starting IKEv2 service... -2025-08-18 16:30:04,693 [INFO] Outline VPN server started successfully on port 8388 -2025-08-18 16:30:05,093 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:30:05,188 [INFO] [SYSTEM] IKEv2 server initialized -2025-08-18 16:30:05,273 [INFO] Database initialized successfully -2025-08-18 16:30:05,275 [INFO] [SYSTEM] VPN server initialized and started on 44.200.228.79 -2025-08-18 16:30:05,278 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:30:05,379 [INFO] NAT rules set up for interface eth0 -2025-08-18 16:30:05,379 [INFO] Internet sharing enabled on interface eth0 -2025-08-18 16:30:05,380 [INFO] NAT engine started -2025-08-18 16:30:05,380 [ERROR] Failed to start Traffic Router: [Errno 99] error while attempting to bind on address ('44.200.228.79', 8388): cannot assign requested address -2025-08-18 16:30:05,380 [INFO] Starting IKEv2 service... -2025-08-18 16:30:05,380 [INFO] Outline VPN server started successfully on port 8388 -2025-08-18 16:30:05,387 [INFO] [SYSTEM] IKEv2 server initialized -2025-08-18 16:30:05,487 [INFO] Database initialized successfully -2025-08-18 16:30:05,488 [INFO] [SYSTEM] VPN server initialized and started on 44.200.228.79 -2025-08-18 16:30:05,507 [WARNING] * Debugger is active! -2025-08-18 16:30:05,508 [INFO] * Debugger PIN: 847-303-629 -2025-08-18 16:30:05,613 [INFO] NAT rules set up for interface eth0 -2025-08-18 16:30:05,613 [INFO] Internet sharing enabled on interface eth0 -2025-08-18 16:30:05,613 [INFO] NAT engine started -2025-08-18 16:30:05,614 [ERROR] Failed to start Traffic Router: [Errno 99] error while attempting to bind on address ('44.200.228.79', 8388): cannot assign requested address -2025-08-18 16:30:05,614 [INFO] Starting IKEv2 service... -2025-08-18 16:30:05,614 [INFO] Outline VPN server started successfully on port 8388 -2025-08-18 16:30:37,472 [INFO] 127.0.0.1 - - [18/Aug/2025 16:30:37] "GET / HTTP/1.1" 500 - -2025-08-18 16:30:37,497 [INFO] 127.0.0.1 - - [18/Aug/2025 16:30:37] "GET /?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 - -2025-08-18 16:30:37,499 [INFO] 127.0.0.1 - - [18/Aug/2025 16:30:37] "GET /?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 - -2025-08-18 16:30:37,526 [INFO] 127.0.0.1 - - [18/Aug/2025 16:30:37] "GET /?__debugger__=yes&cmd=resource&f=console.png&s=HoHl7s1rvD5oO3DVfnnY HTTP/1.1" 200 - -2025-08-18 16:30:37,553 [INFO] 127.0.0.1 - - [18/Aug/2025 16:30:37] "GET /?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 - -2025-08-18 16:30:45,618 [INFO] * Detected change in '/home/ubuntu/outline-vpn/outline-vpn/outline-vpn/web/app.py', reloading -2025-08-18 16:30:45,840 [INFO] * Restarting with stat -2025-08-18 16:30:46,398 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:30:46,512 [INFO] [SYSTEM] IKEv2 server initialized -2025-08-18 16:30:46,644 [INFO] Database initialized successfully -2025-08-18 16:30:46,647 [INFO] [SYSTEM] VPN server initialized and started on 44.200.228.79 -2025-08-18 16:30:46,650 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:30:46,739 [INFO] NAT rules set up for interface eth0 -2025-08-18 16:30:46,739 [INFO] Internet sharing enabled on interface eth0 -2025-08-18 16:30:46,739 [INFO] NAT engine started -2025-08-18 16:30:46,739 [ERROR] Failed to start Traffic Router: [Errno 99] error while attempting to bind on address ('44.200.228.79', 8388): cannot assign requested address -2025-08-18 16:30:46,739 [INFO] Starting IKEv2 service... -2025-08-18 16:30:46,740 [INFO] Outline VPN server started successfully on port 8388 -2025-08-18 16:30:46,764 [INFO] [SYSTEM] IKEv2 server initialized -2025-08-18 16:30:46,856 [INFO] Database initialized successfully -2025-08-18 16:30:46,857 [INFO] [SYSTEM] VPN server initialized and started on 44.200.228.79 -2025-08-18 16:30:46,881 [WARNING] * Debugger is active! -2025-08-18 16:30:46,882 [INFO] * Debugger PIN: 847-303-629 -2025-08-18 16:30:46,976 [INFO] NAT rules set up for interface eth0 -2025-08-18 16:30:46,976 [INFO] Internet sharing enabled on interface eth0 -2025-08-18 16:30:46,976 [INFO] NAT engine started -2025-08-18 16:30:46,976 [ERROR] Failed to start Traffic Router: [Errno 99] error while attempting to bind on address ('44.200.228.79', 8388): cannot assign requested address -2025-08-18 16:30:46,977 [INFO] Starting IKEv2 service... -2025-08-18 16:30:46,977 [INFO] Outline VPN server started successfully on port 8388 -2025-08-18 16:31:04,045 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:31:04,144 [INFO] [SYSTEM] IKEv2 server initialized -2025-08-18 16:31:04,234 [INFO] Database initialized successfully -2025-08-18 16:31:04,237 [INFO] [SYSTEM] VPN server initialized and started on 44.200.228.79 -2025-08-18 16:31:04,256 [INFO] WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. - * Running on http://127.0.0.1:5000 -2025-08-18 16:31:04,256 [INFO] Press CTRL+C to quit -2025-08-18 16:31:04,257 [INFO] * Restarting with stat -2025-08-18 16:31:04,335 [INFO] NAT rules set up for interface eth0 -2025-08-18 16:31:04,336 [INFO] Internet sharing enabled on interface eth0 -2025-08-18 16:31:04,336 [INFO] NAT engine started -2025-08-18 16:31:04,336 [ERROR] Failed to start Traffic Router: [Errno 99] error while attempting to bind on address ('44.200.228.79', 8388): cannot assign requested address -2025-08-18 16:31:04,336 [INFO] Starting IKEv2 service... -2025-08-18 16:31:04,336 [INFO] Outline VPN server started successfully on port 8388 -2025-08-18 16:31:04,730 [INFO] [SYSTEM] Initializing VPN server -2025-08-18 16:31:04,832 [INFO] [SYSTEM] IKEv2 server initialized -2025-08-18 16:31:04,924 [INFO] Database initialized successfully -2025-08-18 16:31:04,926 [INFO] [SYSTEM] VPN server initialized and started on 44.200.228.79 -2025-08-18 16:31:04,943 [WARNING] * Debugger is active! -2025-08-18 16:31:04,943 [INFO] * Debugger PIN: 250-455-276 -2025-08-18 16:31:05,032 [INFO] NAT rules set up for interface eth0 -2025-08-18 16:31:05,032 [INFO] Internet sharing enabled on interface eth0 -2025-08-18 16:31:05,032 [INFO] NAT engine started -2025-08-18 16:31:05,032 [ERROR] Failed to start Traffic Router: [Errno 99] error while attempting to bind on address ('44.200.228.79', 8388): cannot assign requested address -2025-08-18 16:31:05,032 [INFO] Starting IKEv2 service... -2025-08-18 16:31:05,033 [INFO] Outline VPN server started successfully on port 8388 -2025-08-18 16:31:37,704 [INFO] 127.0.0.1 - - [18/Aug/2025 16:31:37] "GET / HTTP/1.1" 200 - -2025-08-18 16:31:37,737 [INFO] 127.0.0.1 - - [18/Aug/2025 16:31:37] "GET /static/css/style.css HTTP/1.1" 304 - -2025-08-18 16:31:37,738 [INFO] 127.0.0.1 - - [18/Aug/2025 16:31:37] "GET /static/js/main.js HTTP/1.1" 304 - -2025-08-18 16:31:45,663 [INFO] 127.0.0.1 - - [18/Aug/2025 16:31:45] "GET /signup HTTP/1.1" 200 - -2025-08-18 16:31:45,791 [INFO] 127.0.0.1 - - [18/Aug/2025 16:31:45] "GET /static/css/style.css HTTP/1.1" 304 - -2025-08-18 16:31:45,807 [INFO] 127.0.0.1 - - [18/Aug/2025 16:31:45] "GET /static/js/main.js HTTP/1.1" 304 - -2025-08-18 16:31:46,002 [INFO] 127.0.0.1 - - [18/Aug/2025 16:31:46] "GET /static/css/style.css HTTP/1.1" 304 - -2025-08-18 16:32:17,741 [INFO] 127.0.0.1 - - [18/Aug/2025 16:32:17] "POST /signup HTTP/1.1" 500 - -2025-08-18 16:32:17,763 [INFO] 127.0.0.1 - - [18/Aug/2025 16:32:17] "GET /signup?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 - -2025-08-18 16:32:17,765 [INFO] 127.0.0.1 - - [18/Aug/2025 16:32:17] "GET /signup?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 304 - -2025-08-18 16:32:17,781 [INFO] 127.0.0.1 - - [18/Aug/2025 16:32:17] "GET /signup?__debugger__=yes&cmd=resource&f=console.png&s=SWpKuG4jXHoO71qMEOPo HTTP/1.1" 200 - -2025-08-18 16:32:17,814 [INFO] 127.0.0.1 - - [18/Aug/2025 16:32:17] "GET /signup?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 304 - -2025-08-18 16:32:17,822 [INFO] 127.0.0.1 - - [18/Aug/2025 16:32:17] "GET /signup?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 304 - -2025-08-18 16:32:32,155 [INFO] * Detected change in '/home/ubuntu/outline-vpn/outline-vpn/outline-vpn/web/app.py', reloading -2025-08-18 16:32:32,293 [INFO] * Restarting with stat diff --git a/main.py b/main.py deleted file mode 100644 index 5aa5e1ec289dc24b7728dcf70e4d5917375d4ca4..0000000000000000000000000000000000000000 --- a/main.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Main entry point for Outline/IKEv2 VPN server -""" - -import os -import sys -import asyncio -import logging -import json -import socket -from pathlib import Path -from contextlib import closing - -# Add project root to path -project_root = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, project_root) - -from core.outline_server import OutlineServer -from core.ikev2_server import IKEv2Server as IPsecManager -from core.process_lock import ProcessLock - -# Setup logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.StreamHandler(), - logging.FileHandler('vpn_server.log') - ] -) - -logger = logging.getLogger(__name__) - -def find_available_port(start_port: int, end_port: int = 65535) -> int: - """Find an available port in the given range""" - for port in range(start_port, end_port + 1): - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: - try: - sock.bind(('', port)) - return port - except socket.error: - continue - raise RuntimeError(f"No available ports in range {start_port}-{end_port}") - -async def main(): - # Initialize process lock - process_lock = ProcessLock() - if not process_lock.acquire(): - logger.error("Another instance of the VPN server is already running") - sys.exit(1) - - try: - # Load configuration - config_path = os.path.join(project_root, 'config', 'server_config.json') - if not os.path.exists(config_path): - default_config = { - "server": { - "host": "0.0.0.0", - "port": 8388, # Default Shadowsocks port - "virtual_network": "10.0.0.0/24", - "dns_servers": ["8.8.8.8", "8.8.4.4"], - "fallback_ports": [8389, 8390, 8391, 8392] - } - } - os.makedirs(os.path.dirname(config_path), exist_ok=True) - with open(config_path, 'w') as f: - json.dump(default_config, f, indent=4) - logger.info("Created default configuration") - - # Load configuration - with open(config_path, 'r') as f: - config = json.load(f) - - # Find available port - try: - port = config["server"]["port"] - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: - sock.bind(('', port)) - except socket.error: - logger.warning(f"Default port {port} is in use, finding alternative...") - port = find_available_port(8388) - config["server"]["port"] = port - logger.info(f"Using alternative port: {port}") - - # Start Outline server with elevated privileges check - if os.name != 'nt' and os.geteuid() != 0: # Not running as root on Unix - logger.error("VPN server must be run with administrator/root privileges") - sys.exit(1) - - server = OutlineServer(config) - await server.start() - - # Keep running until interrupted - while True: - await asyncio.sleep(1) - - except KeyboardInterrupt: - logger.info("Shutting down VPN server...") - if 'server' in locals(): - await server.stop() - - except Exception as e: - logger.error(f"Server error: {e}") - sys.exit(1) - - finally: - process_lock.release() - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/models/database.py b/models/database.py deleted file mode 100644 index 0b6aeca9e03a74706d032db926e5893da09502f3..0000000000000000000000000000000000000000 --- a/models/database.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -SQLAlchemy models for VPN server -""" -from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float, Enum, Boolean -from sqlalchemy.orm import relationship -from datetime import datetime -import enum - -from core.database import Base - -class UserRole(str, enum.Enum): - ADMIN = "admin" - USER = "user" - -class UserStatus(str, enum.Enum): - ACTIVE = "active" - INACTIVE = "inactive" - SUSPENDED = "suspended" - -class User(Base): - __tablename__ = "users" - - id = Column(Integer, primary_key=True, index=True) - username = Column(String, unique=True, index=True) - email = Column(String, unique=True, index=True) - password = Column(String) - role = Column(String, default=UserRole.USER) - status = Column(String, default=UserStatus.ACTIVE) - vpn_protocol = Column(String, default="outline") - created_at = Column(DateTime, default=datetime.utcnow) - last_login = Column(DateTime, nullable=True) - - # Relationships - sessions = relationship("VPNSession", back_populates="user") - config = relationship("UserVPNConfig", back_populates="user", uselist=False) - bandwidth_usages = relationship("BandwidthUsage", back_populates="user") - -class VPNSession(Base): - __tablename__ = "vpn_sessions" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id")) - protocol = Column(String) - client_ip = Column(String) - server_ip = Column(String) - start_time = Column(DateTime, default=datetime.utcnow) - end_time = Column(DateTime, nullable=True) - bytes_sent = Column(Integer, default=0) - bytes_received = Column(Integer, default=0) - status = Column(String, default="active") - - # Relationships - user = relationship("User", back_populates="sessions") - -class UserVPNConfig(Base): - __tablename__ = "user_vpn_configs" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id"), unique=True) - protocol = Column(String) - config_data = Column(String) # JSON string of configuration - created_at = Column(DateTime, default=datetime.utcnow) - last_updated = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - # Relationships - user = relationship("User", back_populates="config") - -class BandwidthUsage(Base): - __tablename__ = "bandwidth_usage" - - id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("users.id")) - timestamp = Column(DateTime, default=datetime.utcnow) - bytes_up = Column(Integer, default=0) - bytes_down = Column(Integer, default=0) - protocol = Column(String) - - # Relationships - user = relationship("User", back_populates="bandwidth_usages") - -class ServerConfig(Base): - __tablename__ = "server_config" - - id = Column(Integer, primary_key=True, index=True) - server_name = Column(String, default="VPN Server") - max_clients = Column(Integer, default=100) - bandwidth_limit = Column(Integer, nullable=True) # In bytes per second - maintenance_mode = Column(Boolean, default=False) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - -class AuditLog(Base): - __tablename__ = "audit_logs" - - id = Column(Integer, primary_key=True, index=True) - timestamp = Column(DateTime, default=datetime.utcnow) - user_id = Column(Integer, ForeignKey("users.id"), nullable=True) - action = Column(String) - details = Column(String) - ip_address = Column(String, nullable=True) - - # Relationships - user = relationship("User") diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2a27f8fb8954363faeff1eef412396c1fb52457f..0000000000000000000000000000000000000000 --- a/requirements.txt +++ /dev/null @@ -1,63 +0,0 @@ -# Core dependencies -fastapi>=0.68.0 -uvicorn[standard]>=0.15.0 -aiohttp>=3.8.1 -cryptography>=35.0.0 -python-jose[cryptography]>=3.3.0 -passlib[bcrypt]>=1.7.4 -python-multipart>=0.0.5 -jinja2>=3.0.1 -email-validator>=1.1.3 -SQLAlchemy>=1.4.23 -psutil>=5.9.5 -bcrypt>=3.2.0 - -# Authentication and Security -pyotp>=2.8.0 -qrcode>=7.4.2 -python-jose[cryptography]>=3.3.0 - -# Monitoring and Analytics -APScheduler>=3.10.1 -pytz>=2023.3 -prometheus-client>=0.17.1 - -# Networking -aiohappyeyeballs==2.6.1 -aiosignal==1.4.0 -websockets==15.0.1 -requests==2.31.0 - -# Crypto and security -pyOpenSSL==23.3.0 -shadowsocks-fixed - -# Data Export -pandas==2.0.3 -openpyxl==3.1.2 -XlsxWriter==3.1.2 - -# Database Backup -alembic==1.11.1 -python-dotenv==1.0.0 - -# Testing -pytest==7.4.0 -pytest-asyncio==0.21.1 -pytest-cov==4.1.0 -faker==19.3.0 - -# Frontend -Werkzeug>=3.1.0 -itsdangerous>=2.2.0 -Jinja2==3.1.2 -flask-assets==2.0 -webassets==2.0 -cssmin==0.2.0 -jsmin==3.0.1 - -# Utilities -click==8.2.1 -typing_extensions==4.14.0 -python-dateutil==2.8.2 -humanize==4.7.0 diff --git a/routers/admin.py b/routers/admin.py deleted file mode 100644 index 0af78a92746192b6c4fd6184870857ca76c73adc..0000000000000000000000000000000000000000 --- a/routers/admin.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Admin routes and functionality for VPN Server -""" -from fastapi import APIRouter, Depends, HTTPException, status, Request, File, UploadFile -from fastapi.responses import JSONResponse, FileResponse, StreamingResponse -from fastapi.security import OAuth2PasswordBearer -from sqlalchemy.orm import Session -from typing import List, Optional -from datetime import datetime -import os - -from models.user import User, UserRole -from models.system import SystemHealth, AuditLog, Alert, ServerConfig -from services import backup_service, monitoring_service -from core.auth import get_current_active_user, get_current_admin_user -from core.database import get_db -from schemas.admin import ( - SystemHealthResponse, - ServerConfigUpdate, - AlertResponse, - AuditLogResponse -) - -router = APIRouter( - prefix="/admin", - tags=["admin"], - responses={404: {"description": "Not found"}}, -) - -@router.get("/dashboard", response_model=dict) -async def admin_dashboard( - current_user: User = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - """Admin dashboard data""" - system_health = await monitoring_service.get_system_health() - active_alerts = db.query(Alert).filter(Alert.status == 'active').order_by(Alert.created_at.desc()).all() - audit_logs = db.query(AuditLog).order_by(AuditLog.timestamp.desc()).limit(50).all() - - return { - "system_health": system_health, - "active_alerts": active_alerts, - "audit_logs": audit_logs - } - -@router.get("/system-health", response_model=SystemHealthResponse) -async def get_system_health( - current_user: User = Depends(get_current_admin_user) -): - """Get real-time system health metrics""" - return await monitoring_service.get_system_health() - -@router.post("/server-config", response_model=dict) -async def update_server_config( - config: ServerConfigUpdate, - current_user: User = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - """Update server configuration""" - try: - # Create backup before updating - await backup_service.backup_config('pre_update') - - # Update configuration - current_config = db.query(ServerConfig).first() - for key, value in config.dict(exclude_unset=True).items(): - setattr(current_config, key, value) - - db.commit() - - # Log the change - audit_log = AuditLog( - user_id=current_user.id, - action='update_config', - details='Server configuration updated' - ) - db.add(audit_log) - db.commit() - - # Restart required services - await monitoring_service.restart_services() - - return {"status": "success"} - except Exception as e: - db.rollback() - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - -@router.post("/backup", response_model=dict) -async def create_backup( - include_user_data: bool = False, - current_user: User = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - """Create a backup of server configuration""" - try: - backup_path = await backup_service.create_backup(include_user_data) - - # Log the backup creation - audit_log = AuditLog( - user_id=current_user.id, - action='create_backup', - details=f'Backup created: {os.path.basename(backup_path)}' - ) - db.add(audit_log) - db.commit() - - filename = f'outline_backup_{datetime.now().strftime("%Y%m%d_%H%M%S")}.zip' - return FileResponse( - path=backup_path, - filename=filename, - media_type='application/zip' - ) - except Exception as e: - db.rollback() - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=str(e) - ) - -@router.get("/audit-logs", response_model=List[AuditLogResponse]) -async def get_audit_logs( - limit: int = 50, - current_user: User = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - """Get audit logs""" - logs = db.query(AuditLog).order_by(AuditLog.timestamp.desc()).limit(limit).all() - return logs - -@router.get("/alerts", response_model=List[AlertResponse]) -async def get_alerts( - status: Optional[str] = None, - current_user: User = Depends(get_current_admin_user), - db: Session = Depends(get_db) -): - """Get system alerts""" - query = db.query(Alert) - if status: - query = query.filter(Alert.status == status) - alerts = query.order_by(Alert.created_at.desc()).all() - return alerts diff --git a/routers/users.py b/routers/users.py deleted file mode 100644 index 76de502bdebad80c690f242c0eee9d856a3fe50c..0000000000000000000000000000000000000000 --- a/routers/users.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -User management routes -""" -from fastapi import APIRouter, Depends, HTTPException, status -from fastapi.security import OAuth2PasswordRequestForm -from sqlalchemy.orm import Session -from typing import List - -from core.auth import ( - get_current_active_user, - create_access_token, - verify_password, - get_password_hash -) -from core.database import get_db -from models.user import User, UserRole -from schemas.user import ( - UserCreate, - UserUpdate, - UserResponse, - TokenResponse -) - -router = APIRouter( - prefix="/users", - tags=["users"], - responses={404: {"description": "Not found"}} -) - -@router.post("/token", response_model=TokenResponse) -async def login( - form_data: OAuth2PasswordRequestForm = Depends(), - db: Session = Depends(get_db) -): - """Login user and create access token""" - user = db.query(User).filter(User.username == form_data.username).first() - if not user or not verify_password(form_data.password, user.password): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect username or password", - headers={"WWW-Authenticate": "Bearer"}, - ) - - access_token = create_access_token(data={"sub": user.username}) - return {"access_token": access_token, "token_type": "bearer"} - -@router.get("/me", response_model=UserResponse) -async def read_user_me(current_user: User = Depends(get_current_active_user)): - """Get current user information""" - return current_user - -@router.put("/me", response_model=UserResponse) -async def update_user_me( - user_update: UserUpdate, - current_user: User = Depends(get_current_active_user), - db: Session = Depends(get_db) -): - """Update current user information""" - if user_update.password: - current_user.password = get_password_hash(user_update.password) - if user_update.email: - current_user.email = user_update.email - if user_update.vpn_protocol: - current_user.vpn_protocol = user_update.vpn_protocol - - db.commit() - return current_user - -@router.post("/register", response_model=UserResponse) -async def register_user(user: UserCreate, db: Session = Depends(get_db)): - """Register a new user""" - # Check if username exists - if db.query(User).filter(User.username == user.username).first(): - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Username already registered" - ) - - # Create new user - db_user = User( - username=user.username, - email=user.email, - password=get_password_hash(user.password), - role=UserRole.USER, - vpn_protocol=user.vpn_protocol - ) - - db.add(db_user) - db.commit() - db.refresh(db_user) - - return db_user diff --git a/routers/vpn.py b/routers/vpn.py deleted file mode 100644 index 12ccadcf3f2b576d546c8cc36c6bd85148bf3fc8..0000000000000000000000000000000000000000 --- a/routers/vpn.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -VPN server management routes -""" -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session -from typing import List - -from core.auth import get_current_active_user -from core.database import get_db -from models.user import User -from schemas.vpn import ( - VPNConfigResponse, - VPNSessionResponse, - VPNServerStats -) -from services.vpn_service import VPNService - -router = APIRouter( - prefix="/vpn", - tags=["vpn"], - responses={404: {"description": "Not found"}} -) - -@router.get("/config", response_model=VPNConfigResponse) -async def get_vpn_config( - current_user: User = Depends(get_current_active_user), - db: Session = Depends(get_db) -): - """Get VPN configuration for current user""" - vpn_service = VPNService(db) - return await vpn_service.get_user_config(current_user.id) - -@router.get("/sessions", response_model=List[VPNSessionResponse]) -async def get_vpn_sessions( - current_user: User = Depends(get_current_active_user), - db: Session = Depends(get_db) -): - """Get active VPN sessions for current user""" - vpn_service = VPNService(db) - return await vpn_service.get_user_sessions(current_user.id) - -@router.get("/stats", response_model=VPNServerStats) -async def get_vpn_stats( - current_user: User = Depends(get_current_active_user), - db: Session = Depends(get_db) -): - """Get VPN usage statistics for current user""" - vpn_service = VPNService(db) - return await vpn_service.get_user_stats(current_user.id) - -@router.post("/disconnect/{session_id}") -async def disconnect_vpn_session( - session_id: str, - current_user: User = Depends(get_current_active_user), - db: Session = Depends(get_db) -): - """Disconnect a specific VPN session""" - vpn_service = VPNService(db) - success = await vpn_service.disconnect_session(session_id, current_user.id) - - if not success: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Session not found or already disconnected" - ) - - return {"status": "success", "message": "Session disconnected"} diff --git a/schemas/admin.py b/schemas/admin.py deleted file mode 100644 index 43a1b75d81ed29633736070a3ba313531d50dabf..0000000000000000000000000000000000000000 --- a/schemas/admin.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Admin route schemas and models -""" -from pydantic import BaseModel, Field -from typing import List, Optional, Dict -from datetime import datetime - -class SystemHealthResponse(BaseModel): - cpu_usage: float - memory_usage: float - disk_usage: float - network_stats: Dict - active_connections: int - uptime: float - - class Config: - orm_mode = True - -class ServerConfigUpdate(BaseModel): - server_name: Optional[str] = None - max_clients: Optional[int] = Field(None, gt=0) - bandwidth_limit: Optional[int] = Field(None, gt=0) - logging_level: Optional[str] = None - maintenance_mode: Optional[bool] = None - - class Config: - orm_mode = True - -class AlertResponse(BaseModel): - id: int - type: str - message: str - status: str - created_at: datetime - updated_at: datetime - - class Config: - orm_mode = True - -class AuditLogResponse(BaseModel): - id: int - user_id: int - action: str - details: str - timestamp: datetime - - class Config: - orm_mode = True diff --git a/schemas/user.py b/schemas/user.py deleted file mode 100644 index 48fda558e15700cbc2453a910d58c9c595774be6..0000000000000000000000000000000000000000 --- a/schemas/user.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -User-related schemas -""" -from pydantic import BaseModel, EmailStr -from typing import Optional -from datetime import datetime - -class UserBase(BaseModel): - username: str - email: EmailStr - vpn_protocol: str = "outline" - -class UserCreate(UserBase): - password: str - -class UserUpdate(BaseModel): - email: Optional[EmailStr] = None - password: Optional[str] = None - vpn_protocol: Optional[str] = None - -class UserResponse(UserBase): - id: str - role: str - is_active: bool - created_at: datetime - last_login: Optional[datetime] - - class Config: - orm_mode = True - -class TokenResponse(BaseModel): - access_token: str - token_type: str diff --git a/schemas/vpn.py b/schemas/vpn.py deleted file mode 100644 index 53c43f36df7e7d436f599ee3ea6f255b1510d4ed..0000000000000000000000000000000000000000 --- a/schemas/vpn.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -VPN-related schemas -""" -from pydantic import BaseModel -from datetime import datetime -from typing import Optional, Dict, List - -class VPNConfigResponse(BaseModel): - protocol: str - server_address: str - port: int - encryption: str - certificate: Optional[str] = None - private_key: Optional[str] = None - shared_secret: Optional[str] = None - additional_params: Dict[str, str] = {} - - class Config: - orm_mode = True - -class VPNSessionResponse(BaseModel): - session_id: str - start_time: datetime - last_active: datetime - protocol: str - client_ip: str - bytes_sent: int - bytes_received: int - status: str - - class Config: - orm_mode = True - -class VPNServerStats(BaseModel): - total_data_transferred: int - active_sessions: int - total_session_time: int - last_connection: Optional[datetime] - bandwidth_usage: Dict[str, float] - - class Config: - orm_mode = True diff --git a/services/monitoring_service.py b/services/monitoring_service.py deleted file mode 100644 index 213b210f6ff918859897e4cce720d880acfd3bda..0000000000000000000000000000000000000000 --- a/services/monitoring_service.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -System monitoring and health check service -""" -import psutil -import platform -from datetime import datetime, timedelta -from typing import Dict, List -from .models import SystemHealth, Alert, db - -class MonitoringService: - def __init__(self): - self.alert_thresholds = { - 'cpu_usage': 90, - 'memory_usage': 90, - 'disk_usage': 90, - 'failed_login_attempts': 10 - } - - def get_system_health(self) -> Dict: - """Get current system health metrics""" - cpu_usage = psutil.cpu_percent(interval=1) - memory = psutil.virtual_memory() - disk = psutil.disk_usage('/') - - health = SystemHealth( - cpu_usage=cpu_usage, - memory_usage=memory.percent, - disk_usage=disk.percent, - timestamp=datetime.utcnow() - ) - db.session.add(health) - db.session.commit() - - # Check for alerts - self._check_alerts(health) - - return { - 'cpu_usage': cpu_usage, - 'memory_usage': memory.percent, - 'disk_usage': disk.percent, - 'memory_total': memory.total, - 'memory_available': memory.available, - 'disk_total': disk.total, - 'disk_free': disk.free, - 'system_time': datetime.utcnow().isoformat(), - 'system_uptime': self.get_system_uptime() - } - - def get_system_uptime(self) -> str: - """Get system uptime in human readable format""" - uptime = datetime.now() - datetime.fromtimestamp(psutil.boot_time()) - days = uptime.days - hours, remainder = divmod(uptime.seconds, 3600) - minutes, seconds = divmod(remainder, 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") - if seconds > 0 or not parts: - parts.append(f"{seconds}s") - - return " ".join(parts) - - def _check_alerts(self, health: SystemHealth) -> None: - """Check system health metrics and create alerts if needed""" - # CPU Usage Alert - if health.cpu_usage >= self.alert_thresholds['cpu_usage']: - self._create_alert( - 'High CPU Usage', - f'CPU usage is at {health.cpu_usage}%', - 'warning' - ) - - # Memory Usage Alert - if health.memory_usage >= self.alert_thresholds['memory_usage']: - self._create_alert( - 'High Memory Usage', - f'Memory usage is at {health.memory_usage}%', - 'warning' - ) - - # Disk Usage Alert - if health.disk_usage >= self.alert_thresholds['disk_usage']: - self._create_alert( - 'High Disk Usage', - f'Disk usage is at {health.disk_usage}%', - 'warning' - ) - - def _create_alert(self, title: str, message: str, severity: str) -> None: - """Create a new alert if one doesn't already exist""" - # Check if a similar active alert exists - existing_alert = Alert.query.filter_by( - title=title, - status='active' - ).first() - - if not existing_alert: - alert = Alert( - title=title, - message=message, - severity=severity, - status='active', - created_at=datetime.utcnow() - ) - db.session.add(alert) - db.session.commit() - - def get_active_alerts(self) -> List[Alert]: - """Get list of active alerts""" - return Alert.query.filter_by(status='active').all() - - def resolve_alert(self, alert_id: int) -> None: - """Mark an alert as resolved""" - alert = Alert.query.get(alert_id) - if alert: - alert.status = 'resolved' - alert.resolved_at = datetime.utcnow() - db.session.commit() - - def restart_services(self) -> None: - """Restart VPN services after configuration change""" - # TODO: Implement service restart logic - pass - -monitoring_service = MonitoringService() diff --git a/services/vpn_service.py b/services/vpn_service.py deleted file mode 100644 index 393dd3a3464c6cf628bcfe47de2a92d00cb0feea..0000000000000000000000000000000000000000 --- a/services/vpn_service.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -VPN service implementation with database integration -""" -from sqlalchemy.orm import Session -from typing import List, Optional -from datetime import datetime -import json - -from models.database import ( - User, - VPNSession, - UserVPNConfig, - BandwidthUsage, - ServerConfig -) -from core.nat_engine import NATEngine -from core.outline_server import OutlineServer -from core.ikev2_server import IKEv2Server -from schemas.vpn import VPNConfigResponse, VPNSessionResponse, VPNServerStats - -class VPNService: - def __init__(self, db: Session): - self.db = db - self.nat_engine = NATEngine() - self.outline_server = OutlineServer() - self.ikev2_server = IKEv2Server() - - async def get_user_config(self, user_id: int) -> VPNConfigResponse: - """Get VPN configuration for a user""" - user = self.db.query(User).filter(User.id == user_id).first() - if not user: - raise ValueError("User not found") - - # Get or create user's VPN config - config = self.db.query(UserVPNConfig).filter( - UserVPNConfig.user_id == user_id - ).first() - - if not config: - # Generate new configuration based on protocol - if user.vpn_protocol == "outline": - config_data = await self.outline_server.generate_user_config(user_id) - elif user.vpn_protocol == "ikev2": - config_data = await self.ikev2_server.generate_user_config(user_id) - else: - raise ValueError(f"Unsupported VPN protocol: {user.vpn_protocol}") - - # Save configuration to database - config = UserVPNConfig( - user_id=user_id, - protocol=user.vpn_protocol, - config_data=json.dumps(config_data) - ) - self.db.add(config) - self.db.commit() - - config_data = json.loads(config.config_data) - return VPNConfigResponse(**config_data) - - async def get_user_sessions(self, user_id: int) -> List[VPNSessionResponse]: - """Get active VPN sessions for a user""" - sessions = self.db.query(VPNSession).filter( - VPNSession.user_id == user_id, - VPNSession.status == "active" - ).all() - - return [ - VPNSessionResponse( - session_id=str(session.id), - start_time=session.start_time, - last_active=session.end_time or datetime.utcnow(), - protocol=session.protocol, - client_ip=session.client_ip, - bytes_sent=session.bytes_sent, - bytes_received=session.bytes_received, - status=session.status - ) - for session in sessions - ] - - async def get_user_stats(self, user_id: int) -> VPNServerStats: - """Get VPN usage statistics for a user""" - # Get active sessions - active_sessions = self.db.query(VPNSession).filter( - VPNSession.user_id == user_id, - VPNSession.status == "active" - ).all() - - # Get total bytes transferred - bandwidth_usage = self.db.query(BandwidthUsage).filter( - BandwidthUsage.user_id == user_id - ).all() - - total_bytes_up = sum(usage.bytes_up for usage in bandwidth_usage) - total_bytes_down = sum(usage.bytes_down for usage in bandwidth_usage) - - # Get last connection - last_session = self.db.query(VPNSession).filter( - VPNSession.user_id == user_id - ).order_by(VPNSession.start_time.desc()).first() - - # Calculate bandwidth usage by protocol - protocol_usage = {} - for usage in bandwidth_usage: - if usage.protocol not in protocol_usage: - protocol_usage[usage.protocol] = { - 'up': 0, - 'down': 0 - } - protocol_usage[usage.protocol]['up'] += usage.bytes_up - protocol_usage[usage.protocol]['down'] += usage.bytes_down - - return VPNServerStats( - total_data_transferred=total_bytes_up + total_bytes_down, - active_sessions=len(active_sessions), - total_session_time=sum( - int((s.end_time or datetime.utcnow() - s.start_time).total_seconds()) - for s in active_sessions - ), - last_connection=last_session.start_time if last_session else None, - bandwidth_usage=protocol_usage - ) - - async def disconnect_session(self, session_id: str, user_id: int) -> bool: - """Disconnect a specific VPN session""" - session = self.db.query(VPNSession).filter( - VPNSession.id == session_id, - VPNSession.user_id == user_id, - VPNSession.status == "active" - ).first() - - if not session: - return False - - # Disconnect from VPN server - if session.protocol == "outline": - await self.outline_server.disconnect_session(session_id) - elif session.protocol == "ikev2": - await self.ikev2_server.disconnect_session(session_id) - - # Update session status in database - session.status = "disconnected" - session.end_time = datetime.utcnow() - self.db.commit() - - return True - - async def update_bandwidth_usage(self, user_id: int, protocol: str, bytes_up: int, bytes_down: int): - """Update bandwidth usage statistics""" - usage = BandwidthUsage( - user_id=user_id, - protocol=protocol, - bytes_up=bytes_up, - bytes_down=bytes_down - ) - self.db.add(usage) - self.db.commit() diff --git a/static/css/dashboard.css b/static/css/dashboard.css deleted file mode 100644 index 276d277e8ebda2fa38c202f10ae9ae12129811eb..0000000000000000000000000000000000000000 --- a/static/css/dashboard.css +++ /dev/null @@ -1,248 +0,0 @@ -/* Modern Dashboard Styles */ -:root { - --primary: #4F46E5; - --primary-dark: #4338CA; - --secondary: #10B981; - --secondary-dark: #059669; - --background: #F9FAFB; - --surface: #FFFFFF; - --text-primary: #111827; - --text-secondary: #6B7280; - --danger: #EF4444; - --warning: #F59E0B; - --success: #10B981; -} - -body { - background-color: var(--background); - font-family: 'Inter', sans-serif; -} - -/* Animated Cards */ -.dashboard-card { - background: var(--surface); - border-radius: 1rem; - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - transition: all 0.3s ease; - transform: translateY(0); - border: 1px solid rgba(0,0,0,0.05); -} - -.dashboard-card:hover { - transform: translateY(-4px); - box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); -} - -/* Stat Cards */ -.stat-card { - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.stat-card .stat-icon { - width: 2.5rem; - height: 2.5rem; - border-radius: 0.75rem; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 0.5rem; -} - -.stat-card .stat-value { - font-size: 1.875rem; - font-weight: 600; - color: var(--text-primary); - line-height: 1.2; -} - -.stat-card .stat-label { - font-size: 0.875rem; - color: var(--text-secondary); -} - -.stat-card .stat-change { - display: flex; - align-items: center; - gap: 0.25rem; - font-size: 0.875rem; -} - -/* Status Indicators */ -.status-indicator { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.875rem; - font-weight: 500; -} - -.status-indicator.online { - background-color: rgba(16, 185, 129, 0.1); - color: var(--success); -} - -.status-indicator.offline { - background-color: rgba(239, 68, 68, 0.1); - color: var(--danger); -} - -/* Charts and Graphs */ -.chart-container { - padding: 1.5rem; - height: 300px; -} - -/* Session Cards */ -.session-card { - padding: 1.25rem; - display: flex; - align-items: center; - gap: 1rem; - border-bottom: 1px solid rgba(0,0,0,0.05); -} - -.session-card:last-child { - border-bottom: none; -} - -.session-info { - flex: 1; -} - -/* Quick Actions */ -.quick-action { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - border-radius: 0.5rem; - background-color: var(--background); - color: var(--text-primary); - transition: all 0.2s ease; -} - -.quick-action:hover { - background-color: var(--primary); - color: white; -} - -/* Animations */ -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.animate-fade-in { - animation: fadeIn 0.5s ease forwards; -} - -/* Protocol Badges */ -.protocol-badge { - display: inline-flex; - align-items: center; - padding: 0.375rem 0.75rem; - border-radius: 0.375rem; - font-size: 0.875rem; - font-weight: 500; - background-color: rgba(79, 70, 229, 0.1); - color: var(--primary); - margin: 0.25rem; -} - -/* Traffic Charts */ -.traffic-chart { - position: relative; - height: 400px; - margin-top: 1rem; -} - -/* Data Cards Grid */ -.data-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1.5rem; - margin-top: 1.5rem; -} - -/* Responsive Sidebar */ -.dashboard-sidebar { - position: fixed; - left: 0; - top: 0; - bottom: 0; - width: 250px; - background: var(--surface); - border-right: 1px solid rgba(0,0,0,0.05); - padding: 1.5rem; - transform: translateX(0); - transition: transform 0.3s ease; - z-index: 1000; -} - -@media (max-width: 768px) { - .dashboard-sidebar { - transform: translateX(-100%); - } - - .dashboard-sidebar.active { - transform: translateX(0); - } - - .dashboard-main { - margin-left: 0 !important; - } -} - -/* Loading Animations */ -.loading-skeleton { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; -} - -@keyframes loading { - 0% { - background-position: 200% 0; - } - 100% { - background-position: -200% 0; - } -} - -/* Tooltip */ -.tooltip { - position: relative; - display: inline-block; -} - -.tooltip .tooltip-text { - visibility: hidden; - background-color: rgba(0,0,0,0.8); - color: white; - text-align: center; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - position: absolute; - z-index: 1; - bottom: 125%; - left: 50%; - transform: translateX(-50%); - opacity: 0; - transition: opacity 0.2s ease; -} - -.tooltip:hover .tooltip-text { - visibility: visible; - opacity: 1; -} diff --git a/static/css/style.css b/static/css/style.css deleted file mode 100644 index 444fe09b12dca0f855c80512f0300c5573a76e3b..0000000000000000000000000000000000000000 --- a/static/css/style.css +++ /dev/null @@ -1,132 +0,0 @@ -/* Custom styles for Outline VPN */ - -/* Global styles */ -body { - min-height: 100vh; - display: flex; - flex-direction: column; -} - -main { - flex: 1; -} - -/* Card styles */ -.card { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - transition: transform 0.2s ease-in-out; -} - -.card:hover { - transform: translateY(-2px); -} - -/* Dashboard stats cards */ -.stats-card { - border-radius: 10px; - border: none; - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); -} - -.stats-card .card-title { - font-size: 0.9rem; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -/* Navigation */ -.navbar { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.navbar-brand { - font-weight: bold; - letter-spacing: 0.5px; -} - -/* Forms */ -.form-control:focus { - border-color: #0d6efd; - box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); -} - -/* Buttons */ -.btn { - border-radius: 5px; - padding: 0.5rem 1.5rem; - transition: all 0.2s ease-in-out; -} - -.btn-primary { - background: linear-gradient(135deg, #0d6efd 0%, #0a58ca 100%); - border: none; -} - -.btn-primary:hover { - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(13, 110, 253, 0.2); -} - -/* Footer */ -.footer { - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); - margin-top: auto; -} - -/* Dashboard chart container */ -.chart-container { - position: relative; - height: 300px; - width: 100%; -} - -/* Status badges */ -.badge { - padding: 0.5em 1em; - font-weight: 500; - letter-spacing: 0.5px; -} - -/* Quick setup guide */ -.list-group-numbered { - counter-reset: section; -} - -.list-group-numbered > .list-group-item { - display: flex; - align-items: center; -} - -.list-group-numbered > .list-group-item::before { - content: counter(section); - counter-increment: section; - background-color: #e9ecef; - border-radius: 50%; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - margin-right: 1rem; - font-size: 0.875rem; - font-weight: 500; -} - -/* Loading spinner */ -.spinner-border-sm { - width: 1rem; - height: 1rem; - border-width: 0.2em; -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .card { - margin-bottom: 1rem; - } - - .btn { - width: 100%; - margin-bottom: 0.5rem; - } -} diff --git a/static/js/auth.js b/static/js/auth.js deleted file mode 100644 index 4790a596ba931ae8f1e2d1f11e0b09923ecab6fd..0000000000000000000000000000000000000000 --- a/static/js/auth.js +++ /dev/null @@ -1,193 +0,0 @@ -/** - * Client-side form validation and authentication handling - */ - -// Token management functions -function getToken() { - return localStorage.getItem('auth_token'); -} - -function setToken(token) { - localStorage.setItem('auth_token', token); -} - -function removeToken() { - localStorage.removeItem('auth_token'); -} - -// Check authentication state and update UI accordingly -async function checkAuthState() { - try { - const token = getToken(); - const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; - - const response = await fetch('/api/user/current', { headers }); - if (!response.ok) { - throw new Error('Auth check failed'); - } - const user = await response.json(); - - const loggedIn = document.querySelector('.auth-logged-in'); - const loggedOut = document.querySelector('.auth-logged-out'); - const userProfile = document.querySelector('.user-profile'); - - if (user && user.username) { - loggedIn.style.display = 'block'; - loggedOut.style.display = 'none'; - userProfile.textContent = user.username; - } else { - loggedIn.style.display = 'none'; - loggedOut.style.display = 'block'; - } - } catch (error) { - console.error('Auth check failed:', error); - document.querySelector('.auth-logged-in').style.display = 'none'; - document.querySelector('.auth-logged-out').style.display = 'block'; - removeToken(); - } -} - -document.addEventListener('DOMContentLoaded', function() { - // Check auth state when page loads - checkAuthState(); - - // Set up form validation listeners - const loginForm = document.getElementById('login-form'); - if (loginForm) { - loginForm.addEventListener('submit', handleLoginSubmit); - } - - const signupForm = document.getElementById('signup-form'); - if (signupForm) { - signupForm.addEventListener('submit', handleSignupSubmit); - } - - // Handle logout - const logoutLink = document.querySelector('a[href="/logout"]'); - if (logoutLink) { - logoutLink.addEventListener('click', function(e) { - e.preventDefault(); - removeToken(); - window.location.href = '/'; - }); - } -}); - -async function handleLoginSubmit(e) { - e.preventDefault(); - const email = document.getElementById('email').value; - const password = document.getElementById('password').value; - - if (!validateLoginForm(email, password)) { - return; - } - - try { - const response = await fetch('/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: `username=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}` - }); - - if (!response.ok) { - throw new Error('Login failed'); - } - - const data = await response.json(); - setToken(data.access_token); - window.location.href = '/dashboard'; - } catch (error) { - console.error('Login error:', error); - showError('Login failed. Please check your credentials and try again.'); - } -} - -async function handleSignupSubmit(e) { - e.preventDefault(); - const email = document.getElementById('email').value; - const password = document.getElementById('password').value; - const confirmPassword = document.getElementById('confirm_password').value; - - if (!validateSignupForm(email, password, confirmPassword)) { - return; - } - - try { - const response = await fetch('/signup', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: email, - password: password - }) - }); - - if (!response.ok) { - throw new Error('Signup failed'); - } - - const data = await response.json(); - setToken(data.access_token); - window.location.href = '/dashboard'; - } catch (error) { - console.error('Signup error:', error); - showError('Signup failed. Please try again.'); - } -} - -function validateLoginForm(email, password) { - if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { - showError('Please enter a valid email address'); - return false; - } - - if (!password || password.length < 8) { - showError('Password must be at least 8 characters long'); - return false; - } - - return true; -} - -function validateSignupForm(email, password, confirmPassword) { - if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { - showError('Please enter a valid email address'); - return false; - } - - if (!password || password.length < 8) { - showError('Password must be at least 8 characters long'); - return false; - } - - if (password !== confirmPassword) { - showError('Passwords do not match'); - return false; - } - - return true; -} - -function showError(message) { - // Remove any existing error alerts - const existingAlert = document.querySelector('.alert-danger'); - if (existingAlert) { - existingAlert.remove(); - } - - // Create and show new error alert - const alert = document.createElement('div'); - alert.className = 'alert alert-danger alert-dismissible fade show'; - alert.role = 'alert'; - alert.innerHTML = ` - ${message} - - `; - - const form = document.querySelector('form'); - form.parentNode.insertBefore(alert, form); -} diff --git a/static/js/dashboard.js b/static/js/dashboard.js deleted file mode 100644 index 0740f8116de1b46ed5ae112c4bad7512292bfbc4..0000000000000000000000000000000000000000 --- a/static/js/dashboard.js +++ /dev/null @@ -1,254 +0,0 @@ -/** - * Dashboard animations and interactivity - */ - -// Initialize animations when DOM is loaded -document.addEventListener('DOMContentLoaded', () => { - initializeAnimations(); - initializeCharts(); - initializeEventListeners(); -}); - -// Animation utilities -const animate = { - fadeIn: (element, delay = 0) => { - element.style.opacity = '0'; - element.style.transform = 'translateY(20px)'; - - setTimeout(() => { - element.style.transition = 'all 0.5s ease'; - element.style.opacity = '1'; - element.style.transform = 'translateY(0)'; - }, delay); - }, - - slideIn: (element, direction = 'left', delay = 0) => { - const transforms = { - left: 'translateX(-50px)', - right: 'translateX(50px)', - up: 'translateY(50px)', - down: 'translateY(-50px)' - }; - - element.style.opacity = '0'; - element.style.transform = transforms[direction]; - - setTimeout(() => { - element.style.transition = 'all 0.5s ease'; - element.style.opacity = '1'; - element.style.transform = 'translate(0)'; - }, delay); - }, - - pulse: (element) => { - element.style.transform = 'scale(1)'; - element.style.transition = 'transform 0.2s ease'; - - element.style.transform = 'scale(1.05)'; - setTimeout(() => { - element.style.transform = 'scale(1)'; - }, 200); - } -}; - -// Initialize animations -function initializeAnimations() { - // Animate stat cards - const statCards = document.querySelectorAll('.stat-card'); - statCards.forEach((card, index) => { - animate.fadeIn(card, index * 100); - }); - - // Animate charts - const charts = document.querySelectorAll('.chart-container'); - charts.forEach((chart, index) => { - animate.slideIn(chart, 'up', 300 + index * 100); - }); - - // Animate session cards - const sessionCards = document.querySelectorAll('.session-card'); - sessionCards.forEach((card, index) => { - animate.slideIn(card, 'right', 500 + index * 100); - }); -} - -// Initialize Charts -function initializeCharts() { - // Traffic Chart - const trafficChart = document.getElementById('traffic-chart'); - if (trafficChart) { - new Chart(trafficChart, { - type: 'line', - data: { - labels: getLastNHours(24), - datasets: [{ - label: 'Upload', - data: generateRandomData(24), - borderColor: '#4F46E5', - tension: 0.4 - }, { - label: 'Download', - data: generateRandomData(24), - borderColor: '#10B981', - tension: 0.4 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'top' - } - }, - scales: { - y: { - beginAtZero: true - } - } - } - }); - } - - // Protocol Distribution Chart - const protocolChart = document.getElementById('protocol-chart'); - if (protocolChart) { - new Chart(protocolChart, { - type: 'doughnut', - data: { - labels: ['Shadowsocks', 'IKEv2', 'L2TP', 'PPTP'], - datasets: [{ - data: [40, 30, 20, 10], - backgroundColor: [ - '#4F46E5', - '#10B981', - '#F59E0B', - '#EF4444' - ] - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'bottom' - } - } - } - }); - } -} - -// Initialize Event Listeners -function initializeEventListeners() { - // Quick Action Buttons - const quickActions = document.querySelectorAll('.quick-action'); - quickActions.forEach(action => { - action.addEventListener('mouseenter', () => { - animate.pulse(action); - }); - }); - - // Responsive Sidebar Toggle - const sidebarToggle = document.getElementById('sidebar-toggle'); - const sidebar = document.querySelector('.dashboard-sidebar'); - if (sidebarToggle && sidebar) { - sidebarToggle.addEventListener('click', () => { - sidebar.classList.toggle('active'); - }); - } - - // Live Data Updates - setInterval(updateLiveStats, 5000); -} - -// Utility Functions -function getLastNHours(n) { - const hours = []; - for (let i = n - 1; i >= 0; i--) { - const d = new Date(); - d.setHours(d.getHours() - i); - hours.push(d.getHours() + ':00'); - } - return hours; -} - -function generateRandomData(n) { - return Array.from({length: n}, () => Math.floor(Math.random() * 100)); -} - -function formatBytes(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -} - -// Live Data Updates -function updateLiveStats() { - fetch('/api/stats') - .then(response => response.json()) - .then(data => { - // Update connection status - const statusIndicator = document.querySelector('.status-indicator'); - if (statusIndicator) { - statusIndicator.className = `status-indicator ${data.isConnected ? 'online' : 'offline'}`; - statusIndicator.textContent = data.isConnected ? 'Connected' : 'Disconnected'; - } - - // Update traffic stats - const uploadStat = document.getElementById('upload-stat'); - const downloadStat = document.getElementById('download-stat'); - if (uploadStat) uploadStat.textContent = formatBytes(data.uploadTotal); - if (downloadStat) downloadStat.textContent = formatBytes(data.downloadTotal); - - // Update active sessions - updateSessionsList(data.sessions); - }) - .catch(console.error); -} - -function updateSessionsList(sessions) { - const sessionsList = document.getElementById('active-sessions'); - if (!sessionsList) return; - - sessions.forEach(session => { - const existingSession = document.getElementById(`session-${session.id}`); - if (!existingSession) { - // Create new session card - const sessionCard = createSessionCard(session); - animate.slideIn(sessionCard, 'right'); - sessionsList.appendChild(sessionCard); - } else { - // Update existing session - updateSessionCard(existingSession, session); - } - }); -} - -function createSessionCard(session) { - const card = document.createElement('div'); - card.id = `session-${session.id}`; - card.className = 'session-card dashboard-card'; - card.innerHTML = ` -
-
${session.protocol}
- ${session.ip} -
-
-
↑ ${formatBytes(session.upload)}
-
↓ ${formatBytes(session.download)}
-
- `; - return card; -} - -function updateSessionCard(card, session) { - const uploadEl = card.querySelector('.session-stats div:first-child'); - const downloadEl = card.querySelector('.session-stats div:last-child'); - - uploadEl.textContent = `↑ ${formatBytes(session.upload)}`; - downloadEl.textContent = `↓ ${formatBytes(session.download)}`; -} diff --git a/static/js/main.js b/static/js/main.js deleted file mode 100644 index 0935d090055b52ce6c75f778b3b6afa1d92e7138..0000000000000000000000000000000000000000 --- a/static/js/main.js +++ /dev/null @@ -1,149 +0,0 @@ -// Main JavaScript file for Outline VPN - -// Helper function to format bytes to human-readable format -function formatBytes(bytes, decimals = 2) { - if (bytes === 0) return '0 Bytes'; - - const k = 1024; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - - const i = Math.floor(Math.log(bytes) / Math.log(k)); - - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; -} - -// Helper function to format duration -function formatDuration(milliseconds) { - const seconds = Math.floor(milliseconds / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (days > 0) return `${days}d ${hours % 24}h`; - if (hours > 0) return `${hours}h ${minutes % 60}m`; - if (minutes > 0) return `${minutes}m ${seconds % 60}s`; - return `${seconds}s`; -} - -// Show toast notification -function showToast(message, type = 'info') { - const toast = document.createElement('div'); - toast.className = `toast align-items-center text-white bg-${type} border-0`; - toast.setAttribute('role', 'alert'); - toast.setAttribute('aria-live', 'assertive'); - toast.setAttribute('aria-atomic', 'true'); - - toast.innerHTML = ` -
-
${message}
- -
- `; - - const container = document.createElement('div'); - container.className = 'toast-container position-fixed bottom-0 end-0 p-3'; - container.appendChild(toast); - document.body.appendChild(container); - - const bsToast = new bootstrap.Toast(toast); - bsToast.show(); - - toast.addEventListener('hidden.bs.toast', () => { - container.remove(); - }); -} - -// Copy text to clipboard -function copyToClipboard(text, successMessage = 'Copied to clipboard!') { - navigator.clipboard.writeText(text) - .then(() => showToast(successMessage, 'success')) - .catch(() => showToast('Failed to copy text', 'danger')); -} - -// Download file helper -function downloadFile(content, filename, type = 'application/json') { - const blob = new Blob([content], { type }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); -} - -// Form validation -function validateForm(formElement) { - const requiredFields = formElement.querySelectorAll('[required]'); - let isValid = true; - - requiredFields.forEach(field => { - if (!field.value.trim()) { - isValid = false; - field.classList.add('is-invalid'); - } else { - field.classList.remove('is-invalid'); - } - }); - - return isValid; -} - -// Password strength checker -function checkPasswordStrength(password) { - let strength = 0; - const messages = []; - - if (password.length >= 8) strength++; - else messages.push('Password should be at least 8 characters long'); - - if (password.match(/[a-z]/)) strength++; - if (password.match(/[A-Z]/)) strength++; - else messages.push('Include at least one uppercase letter'); - - if (password.match(/[0-9]/)) strength++; - else messages.push('Include at least one number'); - - if (password.match(/[^a-zA-Z0-9]/)) strength++; - else messages.push('Include at least one special character'); - - return { - score: strength, - messages: messages, - label: ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong'][strength - 1] || 'Very Weak' - }; -} - -// Initialize password strength meter if it exists -document.addEventListener('DOMContentLoaded', () => { - const passwordInput = document.querySelector('input[type="password"]'); - if (passwordInput) { - const feedbackDiv = document.createElement('div'); - feedbackDiv.className = 'password-strength-meter mt-2'; - passwordInput.parentNode.appendChild(feedbackDiv); - - passwordInput.addEventListener('input', (e) => { - const strength = checkPasswordStrength(e.target.value); - const color = ['danger', 'warning', 'info', 'primary', 'success'][strength.score - 1] || 'danger'; - - feedbackDiv.innerHTML = ` -
-
-
-
- ${strength.label} - `; - }); - } -}); - -// Initialize tooltips -document.addEventListener('DOMContentLoaded', () => { - const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); - tooltipTriggerList.map(function (tooltipTriggerEl) { - return new bootstrap.Tooltip(tooltipTriggerEl); - }); -}); diff --git a/templates/admin.html b/templates/admin.html deleted file mode 100644 index 977d00981753b5a0a0983a22e734fbff2030e84f..0000000000000000000000000000000000000000 --- a/templates/admin.html +++ /dev/null @@ -1,247 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Server Administration - Outline VPN{% endblock %} - -{% block content %} -
- {% include 'sidebar.html' %} - -
-
-
-

Server Administration

-
- - -
-
- -
- -
-
-

Server Configuration

-
- -
-

Network Settings

-
-
- - -
-
- - -
-
-
- - -
-

Protocol Settings

-
-
- - -
-
- - -
-
-
- - -
-

Security Settings

-
-
- - -
-
- - -
-
-
- -
- -
-
-
- - -
-
-

Audit Log

-
- - -
-
-
- - - - - - - - - - - {% for log in audit_logs %} - - - - - - - {% endfor %} - -
TimestampUserActionDetails
{{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}{{ log.username }}{{ log.action }}{{ log.details }}
-
-
-
- - - -
-

Active Alerts

-
- {% for alert in active_alerts %} -
- -
- {{ alert.title }} -

{{ alert.message }}

- {{ alert.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} -
-
- {% endfor %} - {% if not active_alerts %} -

No active alerts

- {% endif %} -
-
-
-
-
- - - - - - - - -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/base.html b/templates/base.html deleted file mode 100644 index 5d55788bc3dae237c5d157c0fd098ab419da26fa..0000000000000000000000000000000000000000 --- a/templates/base.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - {% block title %}Outline VPN{% endblock %} - - - - - - {% block extra_css %}{% endblock %} - - - - - -
- {% if messages %} - {% for message in messages %} - - {% endfor %} - {% endif %} - - {% block content %}{% endblock %} -
- - - - - - - - - - {% block extra_js %}{% endblock %} - - diff --git a/templates/dashboard.html b/templates/dashboard.html deleted file mode 100644 index 8611fedf15b802379753296c408867337d222616..0000000000000000000000000000000000000000 --- a/templates/dashboard.html +++ /dev/null @@ -1,662 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Dashboard - Outline VPN{% endblock %} - -{% block extra_css %} - - - -{% endblock %} - -{% block content %} -
- - - - -
-
-

Welcome back, {{ current_user.username }}

- -
- - -
-
-

Connection Status

-
- - Connected -
-
- - Uptime: 12h 30m -
-
- -
-

Data Usage

-
- 450.5 GB -
-
- - +2.3% from last week -
-
- -
-

Active Sessions

-
- 3 -
-
- - Across all devices -
-
- -
-

Network Speed

-
- 125 Mbps -
-
- - Average speed -
-
-
- - -
-

Traffic Overview

- -
- - -
-
-

Active Sessions

- -
-
- - - - - - - - - - - - - {% for session in active_sessions %} - - - - - - - - - {% endfor %} - -
DeviceIP AddressLocationConnected SinceStatusActions
-
- - {{ session.device_name }} -
-
{{ session.ip_address }}{{ session.location }}{{ session.connected_since }} - - {{ session.status|title }} - - -
-
-
- - -
-
-
-

Quick Actions

-
- - - -
-
-
-
-
-

System Status

-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-{% endblock %} - -{% block extra_js %} - - - - -{% endblock %} -
-
-
-
Data Upload
-

0 B

-
-
-
-
-
-
-
-
-
-
Data Download
-

0 B

-
-
-
-
-
-
-
-
-
-
Connected Since
-

-

- -
-
-
- - - -
-
-
Active Sessions
-
-
-
- -
-
-
- - -
- -
-
-
Quick Actions
-
-
-
- - -
-
-
- - -
-
-
Active Protocols
-
-
-
- -
-
-
-
- - - - - -{% endblock %} - -{% block scripts %} - -{% endblock %} -
-
-
-
Usage History
- -
-
-
- - -
-
-
-
-
VPN Configuration
-
- -
- - -
-
-
- -
- - -
-
- -
-
-
-
-
-
-
Quick Setup Guide
-
    -
  1. Download the Outline Client for your device
  2. -
  3. Copy your access key from this dashboard
  4. -
  5. Open the Outline Client and paste your access key
  6. -
  7. Click "Connect" to start using the VPN
  8. -
- -
-
-
-
-{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/forgot_password.html b/templates/forgot_password.html deleted file mode 100644 index e7e78584ff22ddef914617e1c6874c886c5950a3..0000000000000000000000000000000000000000 --- a/templates/forgot_password.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Forgot Password - Outline VPN{% endblock %} - -{% block content %} -
-
-
-
-

Reset Password

-

Enter your email address and we'll send you instructions to reset your password.

-
-
- - -
-
- - Back to Login -
-
-
-
-
-
-{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index ebc624c3b361443d01ee6ae6d45c8aeadeab05b1..0000000000000000000000000000000000000000 --- a/templates/index.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Welcome to Outline VPN{% endblock %} - -{% block content %} -
-

Welcome to Outline VPN

-
-

- Secure, fast, and reliable VPN service for all your needs. - Access the internet freely and privately with our advanced VPN solution. -

-
- {% if user and user is not none %} - Go to Dashboard - {% else %} - Get Started - Sign In - {% endif %} -
-
-
- -
-
-
-
-
-
Secure Connection
-

- Military-grade encryption keeps your data safe and private. -

-
-
-
-
-
-
-
High Speed
-

- Optimized servers ensure fast and reliable connections. -

-
-
-
-
-
-
-
Easy to Use
-

- Simple setup and user-friendly interface for all devices. -

-
-
-
-
-
-{% endblock %} diff --git a/templates/login.html b/templates/login.html deleted file mode 100644 index ecb06641444d4c1f3048f5816545a86904c5cdb3..0000000000000000000000000000000000000000 --- a/templates/login.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Login - Outline VPN{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
-
-
-
-

Login

-
-
- - -
-
- - - -
-
- - -
-
- - Forgot Password? -
-
-
-

Don't have an account? Sign up

-
-
-
-
-
-{% endblock %} - -{% block extra_js %} - - -{% endblock %} diff --git a/templates/profile.html b/templates/profile.html deleted file mode 100644 index 1e6d483837ead666f6411d426c9e0778d91936c7..0000000000000000000000000000000000000000 --- a/templates/profile.html +++ /dev/null @@ -1,235 +0,0 @@ -{% extends "base.html" %} - -{% block title %}User Profile - Outline VPN{% endblock %} - -{% block content %} -
- {% include 'sidebar.html' %} - -
-
-

User Profile

- -
- -
-
-
-

Profile Information

- -
- -
-
-

Username

-

{{ current_user.username }}

-
-
-

Role

-

{{ current_user.role.value|title }}

-
-
-

Account Status

-

- - {{ current_user.status.value|title }} -

-
-
-

Member Since

-

{{ current_user.created_at.strftime('%B %d, %Y') }}

-
-
-
- - -
-

Security Settings

- - -
-
-
-

Password

-

Last changed {{ current_user.password_changed_at.strftime('%B %d, %Y') if current_user.password_changed_at else 'Never' }}

-
- -
-
- - -
-
-
-

Two-Factor Authentication

-

Add an extra layer of security to your account

-
- -
-
-
-
- - -
-
-

Active Sessions

-
- {% for session in user_sessions %} -
-
-
-
{{ session.device_info }}
- {{ session.ip_address }} -
- {% if session.is_current %} - Current - {% endif %} -
-
- Last active: {{ session.last_active.strftime('%B %d, %Y %H:%M') }} -
- {% if not session.is_current %} - - {% endif %} -
- {% endfor %} -
-
-
-
-
-
-
- - - - - - - - - -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/templates/signup.html b/templates/signup.html deleted file mode 100644 index 0f050db23be39b039fdd3bcff21379ad02422a0e..0000000000000000000000000000000000000000 --- a/templates/signup.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Sign Up - Outline VPN{% endblock %} - -{% block content %} -
-
-
-
-

Create Account

-
-
- - -
-
- - -
-
- - -
-
- -
-
-
-

Already have an account? Sign in

-
-
-
-
-
- -{% block extra_js %} - -{% endblock %} -{% endblock %} diff --git a/tests/test_outline.py b/tests/test_outline.py deleted file mode 100644 index 312f9e8e95f3e3f4cfd61705a4d1b2d46a0032a9..0000000000000000000000000000000000000000 --- a/tests/test_outline.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -Test suite for Outline VPN implementation -""" - -import os -import sys -import asyncio -import unittest -import tempfile -from unittest.mock import Mock, patch -from pathlib import Path - -# Add project root to path -project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, project_root) - -from core.outline_config import OutlineManager, UserConfig -from core.outline_server import OutlineServer -from core.shadowsocks_protocol import ShadowsocksProtocol -from core.ipsec_manager import IPsecManager -from core.nat_engine import NATEngine -from core.traffic_router import TrafficRouter - -class TestOutlineVPN(unittest.TestCase): - def setUp(self): - self.temp_dir = tempfile.mkdtemp() - self.config_path = os.path.join(self.temp_dir, 'test_config.json') - self.manager = OutlineManager(self.config_path) - - def tearDown(self): - import shutil - shutil.rmtree(self.temp_dir) - - def test_user_management(self): - """Test user creation and key management""" - # Add user - user = self.manager.add_user('testuser') - self.assertIsNotNone(user.access_key) - self.assertEqual(user.user_id, 'testuser') - self.assertIsNone(user.data_limit) - - # Test user retrieval - found_user = self.manager.get_user_by_key(user.access_key) - self.assertEqual(found_user.user_id, 'testuser') - - # Test user removal - self.assertTrue(self.manager.remove_user('testuser')) - self.assertIsNone(self.manager.get_user_by_key(user.access_key)) - - def test_bandwidth_tracking(self): - """Test bandwidth usage tracking""" - user = self.manager.add_user('bandwidthuser', data_limit=1000) - self.manager.update_user_bandwidth('bandwidthuser', 500) - self.assertEqual(user.bandwidth_usage, 500) - self.assertTrue(user.is_active) - - # Test data limit enforcement - self.manager.update_user_bandwidth('bandwidthuser', 600) - self.assertFalse(user.is_active) - -class TestShadowsocksProtocol(unittest.TestCase): - def setUp(self): - self.protocol = ShadowsocksProtocol("test_key") - - def test_encryption(self): - """Test packet encryption and decryption""" - test_data = b"Hello, World!" - encrypted = self.protocol._encrypt_packet(test_data) - decrypted = self.protocol._decrypt_packet(encrypted) - self.assertEqual(test_data, decrypted) - -class TestOutlineServer(unittest.IsolatedAsyncioTestCase): - async def asyncSetUp(self): - config = { - "server": { - "host": "127.0.0.1", - "port": 8388, - "virtual_network": "10.0.0.0/24" - } - } - self.server = OutlineServer(config) - - async def test_server_start_stop(self): - """Test server startup and shutdown""" - # Mock the actual server binding - with patch('asyncio.start_server'): - startup_task = asyncio.create_task(self.server.start()) - await asyncio.sleep(0.1) # Give time for server to "start" - self.assertTrue(self.server.is_running) - - await self.server.stop() - self.assertFalse(self.server.is_running) - await startup_task - - async def test_client_handling(self): - """Test client connection handling""" - # Add a test user - self.server.outline_manager.add_user('testuser') - - # Mock reader/writer - reader = Mock() - writer = Mock() - writer.get_extra_info.return_value = ('127.0.0.1', 12345) - - # Test connection handling - await self.server._handle_client(reader, writer) - writer.close.assert_called_once() - -class TestNATEngine(unittest.IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.nat = NATEngine() - - async def test_nat_session(self): - """Test NAT session creation and tracking""" - session = self.nat.create_session( - virtual_ip="10.0.0.2", - virtual_port=1234, - real_ip="192.168.1.1", - real_port=80 - ) - self.assertIsNotNone(session) - self.assertEqual(session.virtual_ip, "10.0.0.2") - - # Test session lookup - found = self.nat.lookup_session("192.168.1.1", 80) - self.assertEqual(found.virtual_ip, "10.0.0.2") - -class TestTrafficRouter(unittest.IsolatedAsyncioTestCase): - async def asyncSetUp(self): - config = { - "vpn_host": "127.0.0.1", - "vpn_port": 8388, - "virtual_network": "10.0.0.0/24" - } - self.router = TrafficRouter(config) - - async def test_traffic_routing(self): - """Test traffic routing functionality""" - # Mock connection - reader = Mock() - writer = Mock() - writer.get_extra_info.return_value = ('10.0.0.2', 1234) - - # Test connection handling - with patch('asyncio.open_connection'): - await self.router._handle_client_connection(reader, writer) - writer.close.assert_called_once() - -def run_tests(): - """Run all tests""" - loader = unittest.TestLoader() - suite = loader.loadTestsFromModule(sys.modules[__name__]) - runner = unittest.TextTestRunner(verbosity=2) - return runner.run(suite) - -if __name__ == '__main__': - run_tests() diff --git a/vpn_server.log b/vpn_server.log deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/web/admin_routes.py b/web/admin_routes.py deleted file mode 100644 index 429e90f1e418f91fcd66240e86af1ccae2bab606..0000000000000000000000000000000000000000 --- a/web/admin_routes.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -Admin routes and functionality for Outline VPN -""" -import os -import json -import psutil -import zipfile -from datetime import datetime -from flask import jsonify, request, send_file, flash, redirect, url_for -from flask_login import login_required, current_user -from . import app -from .models import User, UserRole, SystemHealth, AuditLog, Alert -from .services import backup_service, monitoring_service - -def admin_required(f): - """Decorator to require admin role for routes""" - @wraps(f) - def decorated_function(*args, **kwargs): - if not current_user.is_authenticated or current_user.role != UserRole.ADMIN: - flash('You need administrator privileges to access this page.') - return redirect(url_for('dashboard')) - return f(*args, **kwargs) - return decorated_function - -@app.route('/admin') -@login_required -@admin_required -def admin_dashboard(): - """Admin dashboard view""" - system_health = monitoring_service.get_system_health() - active_alerts = Alert.query.filter_by(status='active').order_by(Alert.created_at.desc()).all() - audit_logs = AuditLog.query.order_by(AuditLog.timestamp.desc()).limit(50).all() - - return render_template('admin.html', - system_health=system_health, - active_alerts=active_alerts, - audit_logs=audit_logs) - -@app.route('/api/system-health') -@login_required -@admin_required -def get_system_health(): - """Get real-time system health metrics""" - return jsonify(monitoring_service.get_system_health()) - -@app.route('/api/update-server-config', methods=['POST']) -@login_required -@admin_required -def update_server_config(): - """Update server configuration""" - try: - config = request.get_json() - backup_service.backup_config('pre_update') # Create backup before updating - - # Update configuration - current_config = ServerConfig.query.first() - for key, value in config.items(): - setattr(current_config, key, value) - - db.session.commit() - - # Log the change - AuditLog.create( - user_id=current_user.id, - action='update_config', - details='Server configuration updated' - ) - - # Restart required services - monitoring_service.restart_services() - - return jsonify({'status': 'success'}) - except Exception as e: - return jsonify({'status': 'error', 'message': str(e)}), 500 - -@app.route('/api/create-backup') -@login_required -@admin_required -def create_backup(): - """Create a backup of server configuration""" - try: - include_user_data = request.args.get('include_user_data', 'false') == 'true' - backup_path = backup_service.create_backup(include_user_data) - - # Log the backup creation - AuditLog.create( - user_id=current_user.id, - action='create_backup', - details=f'Backup created: {os.path.basename(backup_path)}' - ) - - return send_file( - backup_path, - as_attachment=True, - download_name=f'outline_backup_{datetime.now().strftime("%Y%m%d_%H%M%S")}.zip' - ) - except Exception as e: - flash(f'Error creating backup: {str(e)}', 'error') - return redirect(url_for('admin_dashboard')) - -@app.route('/api/restore-config', methods=['POST']) -@login_required -@admin_required -def restore_config(): - """Restore server configuration from backup""" - try: - if 'backup_file' not in request.files: - flash('No backup file provided', 'error') - return redirect(url_for('admin_dashboard')) - - backup_file = request.files['backup_file'] - if backup_file.filename == '': - flash('No backup file selected', 'error') - return redirect(url_for('admin_dashboard')) - - # Create backup of current configuration - backup_service.backup_config('pre_restore') - - # Restore from backup - backup_service.restore_from_backup(backup_file) - - # Log the restore - AuditLog.create( - user_id=current_user.id, - action='restore_config', - details=f'Configuration restored from {backup_file.filename}' - ) - - flash('Configuration restored successfully', 'success') - return redirect(url_for('admin_dashboard')) - except Exception as e: - flash(f'Error restoring configuration: {str(e)}', 'error') - return redirect(url_for('admin_dashboard')) - -@app.route('/api/export-audit-log') -@login_required -@admin_required -def export_audit_log(): - """Export audit log in specified format""" - format = request.args.get('format', 'csv') - logs = AuditLog.query.order_by(AuditLog.timestamp.desc()).all() - - if format == 'csv': - output = io.StringIO() - writer = csv.writer(output) - writer.writerow(['Timestamp', 'User', 'Action', 'Details']) - for log in logs: - writer.writerow([ - log.timestamp.strftime('%Y-%m-%d %H:%M:%S'), - log.user.username, - log.action, - log.details - ]) - - return Response( - output.getvalue(), - mimetype='text/csv', - headers={'Content-Disposition': 'attachment; filename=audit_log.csv'} - ) - elif format == 'json': - return jsonify([{ - 'timestamp': log.timestamp.strftime('%Y-%m-%d %H:%M:%S'), - 'user': log.user.username, - 'action': log.action, - 'details': log.details - } for log in logs]) diff --git a/web/app.py b/web/app.py deleted file mode 100644 index 369b6be864d85fb7e5351e14de4b357d25ce0cf0..0000000000000000000000000000000000000000 --- a/web/app.py +++ /dev/null @@ -1,648 +0,0 @@ -from flask import Flask, render_template, jsonify, request, redirect, url_for, flash, session -from flask_login import LoginManager, login_user, logout_user, login_required, current_user -from functools import wraps -import asyncio -import threading -import os -import json -import uuid -import bcrypt -from datetime import datetime, timedelta -import logging -from typing import Dict, Optional - - -import sys -from pathlib import Path -sys.path.append(str(Path(__file__).parent.parent)) - -# Import core VPN components -from core.shadowsocks_protocol import ShadowsocksProtocol -from core.tcp_forward import OutlineTCPForwardingEngine -from core.session_tracker import SessionTracker, UnifiedSession, SessionType, SessionState -from core.logger import LogManager, LogCategory, LogLevel -from core.outline_server import OutlineServer -from core.ikev2_server import IKEv2Server -from core.models.user import User, UserSession, UserRole, UserStatus -from core.database import SessionLocal, engine -from core.database_init import init_db -from core.services.user_service import UserService -from core.outline_config import generate_openvpn_certificates, generate_openvpn_config, generate_wireguard_keys - -# For IP detection -import socket -import requests - -app = Flask(__name__) -app.secret_key = os.urandom(24) -app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=30) - -# Initialize Flask-Login -login_manager = LoginManager() -login_manager.init_app(app) -login_manager.login_view = "login" -login_manager.login_message = "Please log in to access this page." - -@login_manager.user_loader -def load_user(user_id): - db = SessionLocal() - try: - return db.query(User).get(int(user_id)) - finally: - db.close() - -# Global VPN server state -vpn_server: Optional[OutlineServer] = None -session_tracker: Optional[SessionTracker] = None -logger: Optional[LogManager] = None - -# Initialize database -init_db() - -CONFIG_DIR = 'config' -USERS_FILE = os.path.join(CONFIG_DIR, 'users.json') -os.makedirs(CONFIG_DIR, exist_ok=True) - -def load_users(): - if os.path.exists(USERS_FILE): - with open(USERS_FILE, 'r') as f: - return json.load(f) - return {} - -def save_users(users): - with open(USERS_FILE, 'w') as f: - json.dump(users, f) - -def get_server_ip(): - """Get the server's public IP address""" - try: - # First try to get public IP from external service - response = requests.get('https://api.ipify.org', timeout=5) - if response.status_code == 200: - return response.text.strip() - except: - pass - - try: - # Try another public IP service as backup - response = requests.get('https://ifconfig.me', timeout=5) - if response.status_code == 200: - return response.text.strip() - except: - pass - - # Fallback: Get local IP - try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(('8.8.8.8', 80)) - local_ip = s.getsockname()[0] - s.close() - return local_ip - except: - # Last resort fallback - return '127.0.0.1' - -def initialize_ikev2_server(): - """Initialize IKEv2 server""" - global ikev2_server - server_ip = get_server_ip() - ikev2_server = IKEv2Server(server_ip, logger) - logger.log(LogLevel.INFO, LogCategory.SYSTEM, "app", "IKEv2 server initialized") - -def initialize_vpn_server(): - """Initialize the VPN server components""" - global vpn_server, session_tracker, logger, ikev2_server - - # Initialize logger - logger = LogManager() - logger.log(LogLevel.INFO, LogCategory.SYSTEM, "app", "Initializing VPN server") - - # Initialize session tracker - session_tracker = SessionTracker() - - # Initialize IKEv2 server - initialize_ikev2_server() - - # Initialize VPN server - server_ip = get_server_ip() - vpn_server_config = { - "server": { - "host": server_ip, # Use automatically detected server IP - "port": 8388, # Default Shadowsocks port - "virtual_network": "10.7.0.0/24", # Virtual network for client IPs - "protocols": { - "shadowsocks": { - "enabled": True, - "port": 8388 - }, - "wireguard": { - "enabled": True, - "port": 51820 - }, - "openvpn": { - "enabled": True, - "port": 1194 - }, - "ikev2": { - "enabled": True, - "port": 500 - } - } - }, - "security": { - "cipher": "aes-256-gcm", - "auth": "sha256", - "enable_perfect_forward_secrecy": True - } - } - vpn_server = OutlineServer(vpn_server_config) - # Start the VPN server in a separate thread - def run_server(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.run_until_complete(vpn_server.start()) - loop.run_forever() - - server_thread = threading.Thread(target=run_server, daemon=True) - server_thread.start() - logger.log(LogLevel.INFO, LogCategory.SYSTEM, "app", f"VPN server initialized and started on {server_ip}") - -def load_users(): - if os.path.exists(USERS_FILE): - with open(USERS_FILE, 'r') as f: - return json.load(f) - return {} - -def save_users(users): - with open(USERS_FILE, 'w') as f: - json.dump(users, f) - -def login_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - if 'user_id' not in session: - return redirect(url_for('login')) - return f(*args, **kwargs) - return decorated_function - -@app.route('/') -def index(): - if 'user_id' in session: - return redirect(url_for('dashboard')) - return render_template('index.html') - -@app.route("/login", methods=["GET", "POST"]) -def login(): - if current_user.is_authenticated: - return redirect(url_for("dashboard")) - if request.method == "POST": - email = request.form.get("email") - password = request.form.get("password") - remember_me = request.form.get("remember_me") == "on" - db = SessionLocal() - try: - user_service = UserService(db) - success, message, user = user_service.authenticate_user(email, password) - - if success: - if user.status == UserStatus.LOCKED: - flash('Your account is locked. Please try again later or contact support.') - return redirect(url_for('login')) - - # Create a new session - ip_address = request.remote_addr - device_info = request.user_agent.string - user_service.create_session(user, ip_address, device_info) - - # Log in the user - login_user(user, remember=remember_me) - - # Record successful login - user.record_login_attempt(success=True) - db.commit() - - next_page = request.args.get('next') - if not next_page or not next_page.startswith('/'): - next_page = url_for('dashboard') - return redirect(next_page) - else: - if user: - # Record failed attempt - user.record_login_attempt(success=False) - db.commit() - flash(message) - return redirect(url_for('login')) - finally: - db.close() - - return render_template('login.html') - -@app.route('/signup', methods=['GET', 'POST']) -def signup(): - if request.method == 'POST': - users = load_users() - email = request.form.get('email') - password = request.form.get('password') - - if email in users: - flash('Email already registered') - return redirect(url_for('signup')) - - # Hash password and create user - hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() - users[email] = { - 'password': hashed, - 'created_at': datetime.now().isoformat(), - 'config_id': str(uuid.uuid4()) - } - save_users(users) - - # Create user's VPN configuration - create_user_config(users[email]['config_id']) - - session['user_id'] = email - return redirect(url_for('dashboard')) - - return render_template('signup.html') - -@app.route('/dashboard') -@login_required -def dashboard(): - users = load_users() - user = users[session['user_id']] - stats = get_user_stats(user['config_id']) - return render_template('dashboard.html', user=user, stats=stats) - -@app.route('/download_config') -@login_required -def download_config(): - users = load_users() - user = users[session['user_id']] - config_path = os.path.join(CONFIG_DIR, f"{user['config_id']}.json") - - if not os.path.exists(config_path): - flash('Configuration not found') - return redirect(url_for('dashboard')) - - with open(config_path, 'r') as f: - config = json.load(f) - - return jsonify(config) - -@app.route('/api/stats') -@login_required -def get_stats(): - users = load_users() - user = users[session['user_id']] - return jsonify(get_user_stats(user['config_id'])) - -def get_server_ip(): - """Get the server's public IP address""" - try: - # First try to get public IP from external service - response = requests.get('https://api.ipify.org') - if response.status_code == 200: - return response.text.strip() - except: - pass - - # Fallback: Get local IP - try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(('8.8.8.8', 80)) - local_ip = s.getsockname()[0] - s.close() - return local_ip - except: - return '127.0.0.1' # Last resort fallback - -def initialize_ikev2_server(): - """Initialize IKEv2 server""" - global ikev2_server - server_ip = get_server_ip() - ikev2_server = IKEv2Server(server_ip, logger) - logger.log(LogLevel.INFO, LogCategory.SYSTEM, "app", "IKEv2 server initialized") - -def generate_ikev2_certificate(config_id: str) -> Dict: - """Generate IKEv2 certificates for a user""" - username = f"user_{config_id[:8]}" - password = str(uuid.uuid4()) - psk = str(uuid.uuid4()) - - try: - cert_data = ikev2_server.add_user(config_id, username, password, psk) - logger.info(LogCategory.SYSTEM, "app", f"Generated IKEv2 certificates for user {config_id}") - return cert_data - except Exception as e: - logger.error(LogCategory.SYSTEM, "app", f"Failed to generate IKEv2 certificates: {e}") - return None - -def create_user_config(config_id): - """Create Outline VPN configuration for a new user""" - if not os.path.exists(CONFIG_DIR): - os.makedirs(CONFIG_DIR) - - server_ip = get_server_ip() - access_key = str(uuid.uuid4()) - - # Outline/Shadowsocks config - ss_config = { - 'id': config_id, - 'server': { - 'host': server_ip, - 'port': 8388 # Shadowsocks port - }, - 'access_key': access_key, - 'protocol': 'shadowsocks', - 'created_at': datetime.now().isoformat() - } - - # IKEv2 config (Windows 10/11, Android 10+) - ikev2_config = { - 'id': f"{config_id}_ikev2", - 'server': { - 'host': server_ip, - 'port': 500 # IKEv2 port - }, - 'credentials': { - 'username': f"user_{config_id[:8]}", - 'password': str(uuid.uuid4()), - }, - 'psk': str(uuid.uuid4()), # Pre-shared key - 'certificate': generate_ikev2_certificate(config_id), - 'protocol': 'ikev2', - 'created_at': datetime.now().isoformat() - } - - # L2TP/IPsec config (Windows, Android) - l2tp_config = { - 'id': f"{config_id}_l2tp", - 'server': { - 'host': server_ip, - 'ports': { - 'l2tp': 1701, - 'ipsec': [500, 4500] # IPsec ports for NAT traversal - } - }, - 'credentials': { - 'username': f"user_{config_id[:8]}", - 'password': str(uuid.uuid4()) - }, - 'ipsec': { - 'psk': str(uuid.uuid4()), # Pre-shared key for IPsec - 'encryption': 'aes-256-cbc', - 'hash': 'sha256' - }, - 'protocol': 'l2tp_ipsec', - 'created_at': datetime.now().isoformat() - } - - # PPTP config (Legacy support - Windows, Android) - pptp_config = { - 'id': f"{config_id}_pptp", - 'server': { - 'host': server_ip, - 'port': 1723 # PPTP port - }, - 'credentials': { - 'username': f"user_{config_id[:8]}", - 'password': str(uuid.uuid4()) - }, - 'protocol': 'pptp', - 'encryption': 'require-mppe', # Maximum PPTP security - 'warning': 'PPTP is considered less secure, use IKEv2 or L2TP/IPsec when possible', - 'created_at': datetime.now().isoformat() - } - - # OpenVPN config (Universal support) - openvpn_config = { - 'id': f"{config_id}_openvpn", - 'server': { - 'host': server_ip, - 'port': 1194, # OpenVPN default port - 'protocol': 'udp' # UDP for better performance - }, - 'credentials': { - 'username': f"user_{config_id[:8]}", - 'password': str(uuid.uuid4()) - }, - 'certificates': generate_openvpn_certificates(config_id), - 'protocol': 'openvpn', - 'created_at': datetime.now().isoformat(), - 'config_file': generate_openvpn_config(config_id, server_ip) - } - - # WireGuard config (Built-in Windows 11, Android, iOS) - wireguard_config = { - 'id': f"{config_id}_wireguard", - 'server': { - 'host': server_ip, - 'port': 51820, # WireGuard default port - 'public_key': generate_wireguard_keys(config_id)['server_public'], - 'allowed_ips': ['0.0.0.0/0', '::/0'] # Route all traffic - }, - 'client': { - 'private_key': generate_wireguard_keys(config_id)['client_private'], - 'public_key': generate_wireguard_keys(config_id)['client_public'], - 'address': f'10.7.0.{2 + len(load_users())}', # Unique IP for each client - 'dns': ['1.1.1.1', '8.8.8.8'] - }, - 'protocol': 'wireguard', - 'created_at': datetime.now().isoformat() - } - - # L2TP/IPsec config (Built-in Windows, Android, iOS) - l2tp_config = { - 'id': f"{config_id}_l2tp", - 'server': { - 'host': server_ip, - 'port': 1701, # L2TP port - }, - 'credentials': { - 'username': f"user_{config_id[:8]}", - 'password': str(uuid.uuid4()) - }, - 'ipsec': { - 'psk': str(uuid.uuid4()) # Pre-shared key for IPsec - }, - 'protocol': 'l2tp_ipsec', - 'created_at': datetime.now().isoformat() - } - - # Combined config with all supported protocols - config = { - 'id': config_id, - 'protocols': { - 'shadowsocks': ss_config, - 'ikev2': ikev2_config, - 'l2tp': l2tp_config, - 'pptp': pptp_config - }, - 'recommended_protocol': { - 'windows': 'ikev2', - 'android': 'ikev2', - 'fallback': 'l2tp' - }, - 'created_at': datetime.now().isoformat() - } - - config_path = os.path.join(CONFIG_DIR, f"{config_id}.json") - with open(config_path, 'w') as f: - json.dump(config, f) - -def get_user_stats(config_id): - """Get real VPN usage statistics for a user from all active sessions""" - try: - if not session_tracker: - logger.error(LogCategory.SYSTEM, "app", "Session tracker not initialized") - return None - - # Get all sessions for this user - user_sessions = session_tracker.get_user_sessions(config_id) - if not user_sessions: - return { - 'bytes_sent': 0, - 'bytes_received': 0, - 'connected_since': None, - 'last_seen': None, - 'status': 'disconnected', - 'active_sessions': [], - 'protocols': [] - } - - # Aggregate stats from all active sessions - total_bytes_sent = 0 - total_bytes_received = 0 - earliest_connection = None - latest_seen = None - active_sessions = [] - used_protocols = set() - - for sess in user_sessions: - # Update totals - total_bytes_sent += sess.bytes_out - total_bytes_received += sess.bytes_in - - # Track connection times - session_start = datetime.fromtimestamp(sess.start_time) - session_last_seen = datetime.fromtimestamp(sess.last_seen) - - if not earliest_connection or session_start < earliest_connection: - earliest_connection = session_start - if not latest_seen or session_last_seen > latest_seen: - latest_seen = session_last_seen - - # Track protocols - used_protocols.add(sess.protocol) - - # Get session details - session_info = { - 'id': sess.session_id, - 'protocol': sess.protocol, - 'assigned_ip': sess.assigned_ip, - 'connected_since': session_start.isoformat(), - 'last_seen': session_last_seen.isoformat(), - 'bytes_sent': sess.bytes_out, - 'bytes_received': sess.bytes_in, - 'is_offline': sess.is_offline - } - active_sessions.append(session_info) - - # Determine overall status - current_time = datetime.now() - is_active = any( - (current_time - datetime.fromtimestamp(s.last_seen)).total_seconds() < 300 # 5 minutes - for s in user_sessions - ) - - status = 'active' if is_active else 'offline' - if not is_active and any(s.is_offline for s in user_sessions): - status = 'offline_available' - - return { - 'bytes_sent': total_bytes_sent, - 'bytes_received': total_bytes_received, - 'connected_since': earliest_connection.isoformat() if earliest_connection else None, - 'last_seen': latest_seen.isoformat() if latest_seen else None, - 'status': status, - 'active_sessions': active_sessions, - 'protocols': list(used_protocols) - } - - except Exception as e: - logger.error(LogCategory.SYSTEM, "app", f"Error getting user stats: {e}") - return None - -@app.route('/logout') -def logout(): - if current_user.is_authenticated: - db = SessionLocal() - try: - # Find and end the current session - current_session = ( - db.query(UserSession) - .filter(UserSession.user_id == current_user.id) - .order_by(UserSession.created_at.desc()) - .first() - ) - if current_session: - current_session.expires_at = datetime.utcnow() - db.commit() - finally: - db.close() - - logout_user() - flash('You have been logged out.') - return redirect(url_for('index')) - -@app.route('/forgot-password', methods=['GET', 'POST']) -def forgot_password(): - if request.method == 'POST': - email = request.form.get('email') - - db = SessionLocal() - try: - user = db.query(User).filter(User.username == email).first() - if user: - # Generate password reset token - user_service = UserService(db) - reset_token = user_service.generate_reset_token() - user.reset_token = reset_token - user.reset_token_expires = datetime.utcnow() + timedelta(hours=24) - db.commit() - - # TODO: Send reset email with token - # For now, just show the token (in production, you'd send this via email) - flash(f'Password reset link has been sent to your email address.') - else: - # To prevent user enumeration, show the same message - flash(f'Password reset link has been sent to your email address.') - - return redirect(url_for('login')) - finally: - db.close() - - return render_template('forgot_password.html') - -if __name__ == '__main__': - # Initialize the VPN server first - initialize_vpn_server() - - # Run Flask development server - app.run(host="0.0.0.0", port=7860, debug=True) - - # If you want to use Uvicorn (production), uncomment these lines and comment out app.run(): - # from asgiref.wsgi import WsgiToAsgi - # asgi_app = WsgiToAsgi(app) - # import uvicorn - # uvicorn.run(asgi_app, host="0.0.0.0", port=7860) - - - -@app.teardown_appcontext -def shutdown_vpn_server(exception=None): - global vpn_server - if vpn_server and vpn_server.is_running: - logger.log(LogLevel.INFO, LogCategory.SYSTEM, "app", "Shutting down VPN server...") - asyncio.run(vpn_server.stop()) - logger.log(LogLevel.INFO, LogCategory.SYSTEM, "app", "VPN server shut down.") diff --git a/web/services/monitoring_service.py b/web/services/monitoring_service.py deleted file mode 100644 index 213b210f6ff918859897e4cce720d880acfd3bda..0000000000000000000000000000000000000000 --- a/web/services/monitoring_service.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -System monitoring and health check service -""" -import psutil -import platform -from datetime import datetime, timedelta -from typing import Dict, List -from .models import SystemHealth, Alert, db - -class MonitoringService: - def __init__(self): - self.alert_thresholds = { - 'cpu_usage': 90, - 'memory_usage': 90, - 'disk_usage': 90, - 'failed_login_attempts': 10 - } - - def get_system_health(self) -> Dict: - """Get current system health metrics""" - cpu_usage = psutil.cpu_percent(interval=1) - memory = psutil.virtual_memory() - disk = psutil.disk_usage('/') - - health = SystemHealth( - cpu_usage=cpu_usage, - memory_usage=memory.percent, - disk_usage=disk.percent, - timestamp=datetime.utcnow() - ) - db.session.add(health) - db.session.commit() - - # Check for alerts - self._check_alerts(health) - - return { - 'cpu_usage': cpu_usage, - 'memory_usage': memory.percent, - 'disk_usage': disk.percent, - 'memory_total': memory.total, - 'memory_available': memory.available, - 'disk_total': disk.total, - 'disk_free': disk.free, - 'system_time': datetime.utcnow().isoformat(), - 'system_uptime': self.get_system_uptime() - } - - def get_system_uptime(self) -> str: - """Get system uptime in human readable format""" - uptime = datetime.now() - datetime.fromtimestamp(psutil.boot_time()) - days = uptime.days - hours, remainder = divmod(uptime.seconds, 3600) - minutes, seconds = divmod(remainder, 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") - if seconds > 0 or not parts: - parts.append(f"{seconds}s") - - return " ".join(parts) - - def _check_alerts(self, health: SystemHealth) -> None: - """Check system health metrics and create alerts if needed""" - # CPU Usage Alert - if health.cpu_usage >= self.alert_thresholds['cpu_usage']: - self._create_alert( - 'High CPU Usage', - f'CPU usage is at {health.cpu_usage}%', - 'warning' - ) - - # Memory Usage Alert - if health.memory_usage >= self.alert_thresholds['memory_usage']: - self._create_alert( - 'High Memory Usage', - f'Memory usage is at {health.memory_usage}%', - 'warning' - ) - - # Disk Usage Alert - if health.disk_usage >= self.alert_thresholds['disk_usage']: - self._create_alert( - 'High Disk Usage', - f'Disk usage is at {health.disk_usage}%', - 'warning' - ) - - def _create_alert(self, title: str, message: str, severity: str) -> None: - """Create a new alert if one doesn't already exist""" - # Check if a similar active alert exists - existing_alert = Alert.query.filter_by( - title=title, - status='active' - ).first() - - if not existing_alert: - alert = Alert( - title=title, - message=message, - severity=severity, - status='active', - created_at=datetime.utcnow() - ) - db.session.add(alert) - db.session.commit() - - def get_active_alerts(self) -> List[Alert]: - """Get list of active alerts""" - return Alert.query.filter_by(status='active').all() - - def resolve_alert(self, alert_id: int) -> None: - """Mark an alert as resolved""" - alert = Alert.query.get(alert_id) - if alert: - alert.status = 'resolved' - alert.resolved_at = datetime.utcnow() - db.session.commit() - - def restart_services(self) -> None: - """Restart VPN services after configuration change""" - # TODO: Implement service restart logic - pass - -monitoring_service = MonitoringService() diff --git a/web/static/css/dashboard.css b/web/static/css/dashboard.css deleted file mode 100644 index 276d277e8ebda2fa38c202f10ae9ae12129811eb..0000000000000000000000000000000000000000 --- a/web/static/css/dashboard.css +++ /dev/null @@ -1,248 +0,0 @@ -/* Modern Dashboard Styles */ -:root { - --primary: #4F46E5; - --primary-dark: #4338CA; - --secondary: #10B981; - --secondary-dark: #059669; - --background: #F9FAFB; - --surface: #FFFFFF; - --text-primary: #111827; - --text-secondary: #6B7280; - --danger: #EF4444; - --warning: #F59E0B; - --success: #10B981; -} - -body { - background-color: var(--background); - font-family: 'Inter', sans-serif; -} - -/* Animated Cards */ -.dashboard-card { - background: var(--surface); - border-radius: 1rem; - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - transition: all 0.3s ease; - transform: translateY(0); - border: 1px solid rgba(0,0,0,0.05); -} - -.dashboard-card:hover { - transform: translateY(-4px); - box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); -} - -/* Stat Cards */ -.stat-card { - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.stat-card .stat-icon { - width: 2.5rem; - height: 2.5rem; - border-radius: 0.75rem; - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 0.5rem; -} - -.stat-card .stat-value { - font-size: 1.875rem; - font-weight: 600; - color: var(--text-primary); - line-height: 1.2; -} - -.stat-card .stat-label { - font-size: 0.875rem; - color: var(--text-secondary); -} - -.stat-card .stat-change { - display: flex; - align-items: center; - gap: 0.25rem; - font-size: 0.875rem; -} - -/* Status Indicators */ -.status-indicator { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.875rem; - font-weight: 500; -} - -.status-indicator.online { - background-color: rgba(16, 185, 129, 0.1); - color: var(--success); -} - -.status-indicator.offline { - background-color: rgba(239, 68, 68, 0.1); - color: var(--danger); -} - -/* Charts and Graphs */ -.chart-container { - padding: 1.5rem; - height: 300px; -} - -/* Session Cards */ -.session-card { - padding: 1.25rem; - display: flex; - align-items: center; - gap: 1rem; - border-bottom: 1px solid rgba(0,0,0,0.05); -} - -.session-card:last-child { - border-bottom: none; -} - -.session-info { - flex: 1; -} - -/* Quick Actions */ -.quick-action { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - border-radius: 0.5rem; - background-color: var(--background); - color: var(--text-primary); - transition: all 0.2s ease; -} - -.quick-action:hover { - background-color: var(--primary); - color: white; -} - -/* Animations */ -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.animate-fade-in { - animation: fadeIn 0.5s ease forwards; -} - -/* Protocol Badges */ -.protocol-badge { - display: inline-flex; - align-items: center; - padding: 0.375rem 0.75rem; - border-radius: 0.375rem; - font-size: 0.875rem; - font-weight: 500; - background-color: rgba(79, 70, 229, 0.1); - color: var(--primary); - margin: 0.25rem; -} - -/* Traffic Charts */ -.traffic-chart { - position: relative; - height: 400px; - margin-top: 1rem; -} - -/* Data Cards Grid */ -.data-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1.5rem; - margin-top: 1.5rem; -} - -/* Responsive Sidebar */ -.dashboard-sidebar { - position: fixed; - left: 0; - top: 0; - bottom: 0; - width: 250px; - background: var(--surface); - border-right: 1px solid rgba(0,0,0,0.05); - padding: 1.5rem; - transform: translateX(0); - transition: transform 0.3s ease; - z-index: 1000; -} - -@media (max-width: 768px) { - .dashboard-sidebar { - transform: translateX(-100%); - } - - .dashboard-sidebar.active { - transform: translateX(0); - } - - .dashboard-main { - margin-left: 0 !important; - } -} - -/* Loading Animations */ -.loading-skeleton { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; -} - -@keyframes loading { - 0% { - background-position: 200% 0; - } - 100% { - background-position: -200% 0; - } -} - -/* Tooltip */ -.tooltip { - position: relative; - display: inline-block; -} - -.tooltip .tooltip-text { - visibility: hidden; - background-color: rgba(0,0,0,0.8); - color: white; - text-align: center; - padding: 0.5rem 1rem; - border-radius: 0.375rem; - position: absolute; - z-index: 1; - bottom: 125%; - left: 50%; - transform: translateX(-50%); - opacity: 0; - transition: opacity 0.2s ease; -} - -.tooltip:hover .tooltip-text { - visibility: visible; - opacity: 1; -} diff --git a/web/static/css/style.css b/web/static/css/style.css deleted file mode 100644 index 444fe09b12dca0f855c80512f0300c5573a76e3b..0000000000000000000000000000000000000000 --- a/web/static/css/style.css +++ /dev/null @@ -1,132 +0,0 @@ -/* Custom styles for Outline VPN */ - -/* Global styles */ -body { - min-height: 100vh; - display: flex; - flex-direction: column; -} - -main { - flex: 1; -} - -/* Card styles */ -.card { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - transition: transform 0.2s ease-in-out; -} - -.card:hover { - transform: translateY(-2px); -} - -/* Dashboard stats cards */ -.stats-card { - border-radius: 10px; - border: none; - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); -} - -.stats-card .card-title { - font-size: 0.9rem; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -/* Navigation */ -.navbar { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.navbar-brand { - font-weight: bold; - letter-spacing: 0.5px; -} - -/* Forms */ -.form-control:focus { - border-color: #0d6efd; - box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); -} - -/* Buttons */ -.btn { - border-radius: 5px; - padding: 0.5rem 1.5rem; - transition: all 0.2s ease-in-out; -} - -.btn-primary { - background: linear-gradient(135deg, #0d6efd 0%, #0a58ca 100%); - border: none; -} - -.btn-primary:hover { - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(13, 110, 253, 0.2); -} - -/* Footer */ -.footer { - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); - margin-top: auto; -} - -/* Dashboard chart container */ -.chart-container { - position: relative; - height: 300px; - width: 100%; -} - -/* Status badges */ -.badge { - padding: 0.5em 1em; - font-weight: 500; - letter-spacing: 0.5px; -} - -/* Quick setup guide */ -.list-group-numbered { - counter-reset: section; -} - -.list-group-numbered > .list-group-item { - display: flex; - align-items: center; -} - -.list-group-numbered > .list-group-item::before { - content: counter(section); - counter-increment: section; - background-color: #e9ecef; - border-radius: 50%; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - margin-right: 1rem; - font-size: 0.875rem; - font-weight: 500; -} - -/* Loading spinner */ -.spinner-border-sm { - width: 1rem; - height: 1rem; - border-width: 0.2em; -} - -/* Responsive adjustments */ -@media (max-width: 768px) { - .card { - margin-bottom: 1rem; - } - - .btn { - width: 100%; - margin-bottom: 0.5rem; - } -} diff --git a/web/static/js/auth.js b/web/static/js/auth.js deleted file mode 100644 index 47b9e18d4aaf77773b54a09dabf74a13a8a5803c..0000000000000000000000000000000000000000 --- a/web/static/js/auth.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Client-side form validation and authentication handling - */ - -document.addEventListener('DOMContentLoaded', function() { - const loginForm = document.getElementById('login-form'); - if (loginForm) { - loginForm.addEventListener('submit', validateLoginForm); - } - - const signupForm = document.getElementById('signup-form'); - if (signupForm) { - signupForm.addEventListener('submit', validateSignupForm); - } -}); - -function validateLoginForm(e) { - const email = document.getElementById('email').value; - const password = document.getElementById('password').value; - const rememberMe = document.getElementById('remember-me')?.checked; - - let isValid = true; - let errorMessage = ''; - - // Email validation - if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { - errorMessage = 'Please enter a valid email address'; - isValid = false; - } - - // Password validation - if (!password || password.length < 8) { - errorMessage = 'Password must be at least 8 characters long'; - isValid = false; - } - - if (!isValid) { - e.preventDefault(); - showError(errorMessage); - } -} - -function validateSignupForm(e) { - const email = document.getElementById('email').value; - const password = document.getElementById('password').value; - const confirmPassword = document.getElementById('confirm_password').value; - - let isValid = true; - let errorMessage = ''; - - // Email validation - if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) { - errorMessage = 'Please enter a valid email address'; - isValid = false; - } - - // Password validation - if (!password || password.length < 8) { - errorMessage = 'Password must be at least 8 characters long'; - isValid = false; - } else if (!/[A-Z]/.test(password)) { - errorMessage = 'Password must contain at least one uppercase letter'; - isValid = false; - } else if (!/[a-z]/.test(password)) { - errorMessage = 'Password must contain at least one lowercase letter'; - isValid = false; - } else if (!/[0-9]/.test(password)) { - errorMessage = 'Password must contain at least one number'; - isValid = false; - } - - // Confirm password - if (password !== confirmPassword) { - errorMessage = 'Passwords do not match'; - isValid = false; - } - - if (!isValid) { - e.preventDefault(); - showError(errorMessage); - } -} - -function showError(message) { - // Remove any existing error alerts - const existingAlert = document.querySelector('.alert-danger'); - if (existingAlert) { - existingAlert.remove(); - } - - // Create and show new error alert - const alert = document.createElement('div'); - alert.className = 'alert alert-danger alert-dismissible fade show'; - alert.role = 'alert'; - alert.innerHTML = ` - ${message} - - `; - - const form = document.querySelector('form'); - form.parentNode.insertBefore(alert, form); -} diff --git a/web/static/js/dashboard.js b/web/static/js/dashboard.js deleted file mode 100644 index 0740f8116de1b46ed5ae112c4bad7512292bfbc4..0000000000000000000000000000000000000000 --- a/web/static/js/dashboard.js +++ /dev/null @@ -1,254 +0,0 @@ -/** - * Dashboard animations and interactivity - */ - -// Initialize animations when DOM is loaded -document.addEventListener('DOMContentLoaded', () => { - initializeAnimations(); - initializeCharts(); - initializeEventListeners(); -}); - -// Animation utilities -const animate = { - fadeIn: (element, delay = 0) => { - element.style.opacity = '0'; - element.style.transform = 'translateY(20px)'; - - setTimeout(() => { - element.style.transition = 'all 0.5s ease'; - element.style.opacity = '1'; - element.style.transform = 'translateY(0)'; - }, delay); - }, - - slideIn: (element, direction = 'left', delay = 0) => { - const transforms = { - left: 'translateX(-50px)', - right: 'translateX(50px)', - up: 'translateY(50px)', - down: 'translateY(-50px)' - }; - - element.style.opacity = '0'; - element.style.transform = transforms[direction]; - - setTimeout(() => { - element.style.transition = 'all 0.5s ease'; - element.style.opacity = '1'; - element.style.transform = 'translate(0)'; - }, delay); - }, - - pulse: (element) => { - element.style.transform = 'scale(1)'; - element.style.transition = 'transform 0.2s ease'; - - element.style.transform = 'scale(1.05)'; - setTimeout(() => { - element.style.transform = 'scale(1)'; - }, 200); - } -}; - -// Initialize animations -function initializeAnimations() { - // Animate stat cards - const statCards = document.querySelectorAll('.stat-card'); - statCards.forEach((card, index) => { - animate.fadeIn(card, index * 100); - }); - - // Animate charts - const charts = document.querySelectorAll('.chart-container'); - charts.forEach((chart, index) => { - animate.slideIn(chart, 'up', 300 + index * 100); - }); - - // Animate session cards - const sessionCards = document.querySelectorAll('.session-card'); - sessionCards.forEach((card, index) => { - animate.slideIn(card, 'right', 500 + index * 100); - }); -} - -// Initialize Charts -function initializeCharts() { - // Traffic Chart - const trafficChart = document.getElementById('traffic-chart'); - if (trafficChart) { - new Chart(trafficChart, { - type: 'line', - data: { - labels: getLastNHours(24), - datasets: [{ - label: 'Upload', - data: generateRandomData(24), - borderColor: '#4F46E5', - tension: 0.4 - }, { - label: 'Download', - data: generateRandomData(24), - borderColor: '#10B981', - tension: 0.4 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'top' - } - }, - scales: { - y: { - beginAtZero: true - } - } - } - }); - } - - // Protocol Distribution Chart - const protocolChart = document.getElementById('protocol-chart'); - if (protocolChart) { - new Chart(protocolChart, { - type: 'doughnut', - data: { - labels: ['Shadowsocks', 'IKEv2', 'L2TP', 'PPTP'], - datasets: [{ - data: [40, 30, 20, 10], - backgroundColor: [ - '#4F46E5', - '#10B981', - '#F59E0B', - '#EF4444' - ] - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { - position: 'bottom' - } - } - } - }); - } -} - -// Initialize Event Listeners -function initializeEventListeners() { - // Quick Action Buttons - const quickActions = document.querySelectorAll('.quick-action'); - quickActions.forEach(action => { - action.addEventListener('mouseenter', () => { - animate.pulse(action); - }); - }); - - // Responsive Sidebar Toggle - const sidebarToggle = document.getElementById('sidebar-toggle'); - const sidebar = document.querySelector('.dashboard-sidebar'); - if (sidebarToggle && sidebar) { - sidebarToggle.addEventListener('click', () => { - sidebar.classList.toggle('active'); - }); - } - - // Live Data Updates - setInterval(updateLiveStats, 5000); -} - -// Utility Functions -function getLastNHours(n) { - const hours = []; - for (let i = n - 1; i >= 0; i--) { - const d = new Date(); - d.setHours(d.getHours() - i); - hours.push(d.getHours() + ':00'); - } - return hours; -} - -function generateRandomData(n) { - return Array.from({length: n}, () => Math.floor(Math.random() * 100)); -} - -function formatBytes(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -} - -// Live Data Updates -function updateLiveStats() { - fetch('/api/stats') - .then(response => response.json()) - .then(data => { - // Update connection status - const statusIndicator = document.querySelector('.status-indicator'); - if (statusIndicator) { - statusIndicator.className = `status-indicator ${data.isConnected ? 'online' : 'offline'}`; - statusIndicator.textContent = data.isConnected ? 'Connected' : 'Disconnected'; - } - - // Update traffic stats - const uploadStat = document.getElementById('upload-stat'); - const downloadStat = document.getElementById('download-stat'); - if (uploadStat) uploadStat.textContent = formatBytes(data.uploadTotal); - if (downloadStat) downloadStat.textContent = formatBytes(data.downloadTotal); - - // Update active sessions - updateSessionsList(data.sessions); - }) - .catch(console.error); -} - -function updateSessionsList(sessions) { - const sessionsList = document.getElementById('active-sessions'); - if (!sessionsList) return; - - sessions.forEach(session => { - const existingSession = document.getElementById(`session-${session.id}`); - if (!existingSession) { - // Create new session card - const sessionCard = createSessionCard(session); - animate.slideIn(sessionCard, 'right'); - sessionsList.appendChild(sessionCard); - } else { - // Update existing session - updateSessionCard(existingSession, session); - } - }); -} - -function createSessionCard(session) { - const card = document.createElement('div'); - card.id = `session-${session.id}`; - card.className = 'session-card dashboard-card'; - card.innerHTML = ` -
-
${session.protocol}
- ${session.ip} -
-
-
↑ ${formatBytes(session.upload)}
-
↓ ${formatBytes(session.download)}
-
- `; - return card; -} - -function updateSessionCard(card, session) { - const uploadEl = card.querySelector('.session-stats div:first-child'); - const downloadEl = card.querySelector('.session-stats div:last-child'); - - uploadEl.textContent = `↑ ${formatBytes(session.upload)}`; - downloadEl.textContent = `↓ ${formatBytes(session.download)}`; -} diff --git a/web/static/js/main.js b/web/static/js/main.js deleted file mode 100644 index 0935d090055b52ce6c75f778b3b6afa1d92e7138..0000000000000000000000000000000000000000 --- a/web/static/js/main.js +++ /dev/null @@ -1,149 +0,0 @@ -// Main JavaScript file for Outline VPN - -// Helper function to format bytes to human-readable format -function formatBytes(bytes, decimals = 2) { - if (bytes === 0) return '0 Bytes'; - - const k = 1024; - const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - - const i = Math.floor(Math.log(bytes) / Math.log(k)); - - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; -} - -// Helper function to format duration -function formatDuration(milliseconds) { - const seconds = Math.floor(milliseconds / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - if (days > 0) return `${days}d ${hours % 24}h`; - if (hours > 0) return `${hours}h ${minutes % 60}m`; - if (minutes > 0) return `${minutes}m ${seconds % 60}s`; - return `${seconds}s`; -} - -// Show toast notification -function showToast(message, type = 'info') { - const toast = document.createElement('div'); - toast.className = `toast align-items-center text-white bg-${type} border-0`; - toast.setAttribute('role', 'alert'); - toast.setAttribute('aria-live', 'assertive'); - toast.setAttribute('aria-atomic', 'true'); - - toast.innerHTML = ` -
-
${message}
- -
- `; - - const container = document.createElement('div'); - container.className = 'toast-container position-fixed bottom-0 end-0 p-3'; - container.appendChild(toast); - document.body.appendChild(container); - - const bsToast = new bootstrap.Toast(toast); - bsToast.show(); - - toast.addEventListener('hidden.bs.toast', () => { - container.remove(); - }); -} - -// Copy text to clipboard -function copyToClipboard(text, successMessage = 'Copied to clipboard!') { - navigator.clipboard.writeText(text) - .then(() => showToast(successMessage, 'success')) - .catch(() => showToast('Failed to copy text', 'danger')); -} - -// Download file helper -function downloadFile(content, filename, type = 'application/json') { - const blob = new Blob([content], { type }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); -} - -// Form validation -function validateForm(formElement) { - const requiredFields = formElement.querySelectorAll('[required]'); - let isValid = true; - - requiredFields.forEach(field => { - if (!field.value.trim()) { - isValid = false; - field.classList.add('is-invalid'); - } else { - field.classList.remove('is-invalid'); - } - }); - - return isValid; -} - -// Password strength checker -function checkPasswordStrength(password) { - let strength = 0; - const messages = []; - - if (password.length >= 8) strength++; - else messages.push('Password should be at least 8 characters long'); - - if (password.match(/[a-z]/)) strength++; - if (password.match(/[A-Z]/)) strength++; - else messages.push('Include at least one uppercase letter'); - - if (password.match(/[0-9]/)) strength++; - else messages.push('Include at least one number'); - - if (password.match(/[^a-zA-Z0-9]/)) strength++; - else messages.push('Include at least one special character'); - - return { - score: strength, - messages: messages, - label: ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong'][strength - 1] || 'Very Weak' - }; -} - -// Initialize password strength meter if it exists -document.addEventListener('DOMContentLoaded', () => { - const passwordInput = document.querySelector('input[type="password"]'); - if (passwordInput) { - const feedbackDiv = document.createElement('div'); - feedbackDiv.className = 'password-strength-meter mt-2'; - passwordInput.parentNode.appendChild(feedbackDiv); - - passwordInput.addEventListener('input', (e) => { - const strength = checkPasswordStrength(e.target.value); - const color = ['danger', 'warning', 'info', 'primary', 'success'][strength.score - 1] || 'danger'; - - feedbackDiv.innerHTML = ` -
-
-
-
- ${strength.label} - `; - }); - } -}); - -// Initialize tooltips -document.addEventListener('DOMContentLoaded', () => { - const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); - tooltipTriggerList.map(function (tooltipTriggerEl) { - return new bootstrap.Tooltip(tooltipTriggerEl); - }); -}); diff --git a/web/templates/admin.html b/web/templates/admin.html deleted file mode 100644 index 5453d7cfcb52d9b6fc246022d68743280abcdda4..0000000000000000000000000000000000000000 --- a/web/templates/admin.html +++ /dev/null @@ -1,289 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Server Administration - Outline VPN{% endblock %} - -{% block content %} -
- {% include 'sidebar.html' %} - -
-
-
-

Server Administration

-
- - -
-
- -
- -
-
-

Server Configuration

-
- -
-

Network Settings

-
-
- - -
-
- - -
-
-
- - -
-

Protocol Settings

-
-
- - -
-
- - -
-
-
- - -
-

Security Settings

-
-
- - -
-
- - -
-
-
- -
- -
-
-
- - -
-
-

Audit Log

-
- - -
-
-
- - - - - - - - - - - {% for log in audit_logs %} - - - - - - - {% endfor %} - -
TimestampUserActionDetails
{{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}{{ log.username }}{{ log.action }}{{ log.details }}
-
-
-
- - -
- -
-

System Health

-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
- - -
-

Active Alerts

-
- {% for alert in active_alerts %} -
- -
- {{ alert.title }} -

{{ alert.message }}

- {{ alert.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} -
-
- {% endfor %} - {% if not active_alerts %} -

No active alerts

- {% endif %} -
-
-
-
-
-
-
- - - - - - -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/web/templates/base.html b/web/templates/base.html deleted file mode 100644 index 8a351c55f4dd243cdbff20b0d7c1efc35658bb51..0000000000000000000000000000000000000000 --- a/web/templates/base.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - {% block title %}Outline VPN{% endblock %} - - - - - - {% block extra_css %}{% endblock %} - - - - - -
- {% with messages = get_flashed_messages() %} - {% if messages %} - {% for message in messages %} - - {% endfor %} - {% endif %} - {% endwith %} - - {% block content %}{% endblock %} -
- - - - - - - - - {% block extra_js %}{% endblock %} - - diff --git a/web/templates/dashboard.html b/web/templates/dashboard.html deleted file mode 100644 index 8611fedf15b802379753296c408867337d222616..0000000000000000000000000000000000000000 --- a/web/templates/dashboard.html +++ /dev/null @@ -1,662 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Dashboard - Outline VPN{% endblock %} - -{% block extra_css %} - - - -{% endblock %} - -{% block content %} -
- - - - -
-
-

Welcome back, {{ current_user.username }}

- -
- - -
-
-

Connection Status

-
- - Connected -
-
- - Uptime: 12h 30m -
-
- -
-

Data Usage

-
- 450.5 GB -
-
- - +2.3% from last week -
-
- -
-

Active Sessions

-
- 3 -
-
- - Across all devices -
-
- -
-

Network Speed

-
- 125 Mbps -
-
- - Average speed -
-
-
- - -
-

Traffic Overview

- -
- - -
-
-

Active Sessions

- -
-
- - - - - - - - - - - - - {% for session in active_sessions %} - - - - - - - - - {% endfor %} - -
DeviceIP AddressLocationConnected SinceStatusActions
-
- - {{ session.device_name }} -
-
{{ session.ip_address }}{{ session.location }}{{ session.connected_since }} - - {{ session.status|title }} - - -
-
-
- - -
-
-
-

Quick Actions

-
- - - -
-
-
-
-
-

System Status

-
-
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-{% endblock %} - -{% block extra_js %} - - - - -{% endblock %} -
-
-
-
Data Upload
-

0 B

-
-
-
-
-
-
-
-
-
-
Data Download
-

0 B

-
-
-
-
-
-
-
-
-
-
Connected Since
-

-

- -
-
-
- - - -
-
-
Active Sessions
-
-
-
- -
-
-
- - -
- -
-
-
Quick Actions
-
-
-
- - -
-
-
- - -
-
-
Active Protocols
-
-
-
- -
-
-
-
- - - - - -{% endblock %} - -{% block scripts %} - -{% endblock %} -
-
-
-
Usage History
- -
-
-
- - -
-
-
-
-
VPN Configuration
-
- -
- - -
-
-
- -
- - -
-
- -
-
-
-
-
-
-
Quick Setup Guide
-
    -
  1. Download the Outline Client for your device
  2. -
  3. Copy your access key from this dashboard
  4. -
  5. Open the Outline Client and paste your access key
  6. -
  7. Click "Connect" to start using the VPN
  8. -
- -
-
-
-
-{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/web/templates/forgot_password.html b/web/templates/forgot_password.html deleted file mode 100644 index e7e78584ff22ddef914617e1c6874c886c5950a3..0000000000000000000000000000000000000000 --- a/web/templates/forgot_password.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Forgot Password - Outline VPN{% endblock %} - -{% block content %} -
-
-
-
-

Reset Password

-

Enter your email address and we'll send you instructions to reset your password.

-
-
- - -
-
- - Back to Login -
-
-
-
-
-
-{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/web/templates/index.html b/web/templates/index.html deleted file mode 100644 index 8a8f04660eaeacf628f208b9be489ee7b0307dc7..0000000000000000000000000000000000000000 --- a/web/templates/index.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Welcome to Outline VPN{% endblock %} - -{% block content %} -
-

Welcome to Outline VPN

-
-

- Secure, fast, and reliable VPN service for all your needs. - Access the internet freely and privately with our advanced VPN solution. -

- -
-
- -
-
-
-
-
-
Secure Connection
-

- Military-grade encryption keeps your data safe and private. -

-
-
-
-
-
-
-
High Speed
-

- Optimized servers ensure fast and reliable connections. -

-
-
-
-
-
-
-
Easy to Use
-

- Simple setup and user-friendly interface for all devices. -

-
-
-
-
-
-{% endblock %} diff --git a/web/templates/login.html b/web/templates/login.html deleted file mode 100644 index ecb06641444d4c1f3048f5816545a86904c5cdb3..0000000000000000000000000000000000000000 --- a/web/templates/login.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Login - Outline VPN{% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
-
-
-
-

Login

-
-
- - -
-
- - - -
-
- - -
-
- - Forgot Password? -
-
-
-

Don't have an account? Sign up

-
-
-
-
-
-{% endblock %} - -{% block extra_js %} - - -{% endblock %} diff --git a/web/templates/profile.html b/web/templates/profile.html deleted file mode 100644 index 1e6d483837ead666f6411d426c9e0778d91936c7..0000000000000000000000000000000000000000 --- a/web/templates/profile.html +++ /dev/null @@ -1,235 +0,0 @@ -{% extends "base.html" %} - -{% block title %}User Profile - Outline VPN{% endblock %} - -{% block content %} -
- {% include 'sidebar.html' %} - -
-
-

User Profile

- -
- -
-
-
-

Profile Information

- -
- -
-
-

Username

-

{{ current_user.username }}

-
-
-

Role

-

{{ current_user.role.value|title }}

-
-
-

Account Status

-

- - {{ current_user.status.value|title }} -

-
-
-

Member Since

-

{{ current_user.created_at.strftime('%B %d, %Y') }}

-
-
-
- - -
-

Security Settings

- - -
-
-
-

Password

-

Last changed {{ current_user.password_changed_at.strftime('%B %d, %Y') if current_user.password_changed_at else 'Never' }}

-
- -
-
- - -
-
-
-

Two-Factor Authentication

-

Add an extra layer of security to your account

-
- -
-
-
-
- - -
-
-

Active Sessions

-
- {% for session in user_sessions %} -
-
-
-
{{ session.device_info }}
- {{ session.ip_address }} -
- {% if session.is_current %} - Current - {% endif %} -
-
- Last active: {{ session.last_active.strftime('%B %d, %Y %H:%M') }} -
- {% if not session.is_current %} - - {% endif %} -
- {% endfor %} -
-
-
-
-
-
-
- - - - - - - - - -{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/web/templates/signup.html b/web/templates/signup.html deleted file mode 100644 index 0f050db23be39b039fdd3bcff21379ad02422a0e..0000000000000000000000000000000000000000 --- a/web/templates/signup.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Sign Up - Outline VPN{% endblock %} - -{% block content %} -
-
-
-
-

Create Account

-
-
- - -
-
- - -
-
- - -
-
- -
-
-
-

Already have an account? Sign in

-
-
-
-
-
- -{% block extra_js %} - -{% endblock %} -{% endblock %}