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] [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
- * Running on http://127.0.0.1:5000
-2025-08-18 16:30:04,609 [INFO] [33mPress CTRL+C to quit[0m
-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] "[35m[1mGET / HTTP/1.1[0m" 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] [31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
- * Running on http://127.0.0.1:5000
-2025-08-18 16:31:04,256 [INFO] [33mPress CTRL+C to quit[0m
-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] "[36mGET /static/css/style.css HTTP/1.1[0m" 304 -
-2025-08-18 16:31:37,738 [INFO] 127.0.0.1 - - [18/Aug/2025 16:31:37] "[36mGET /static/js/main.js HTTP/1.1[0m" 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] "[36mGET /static/css/style.css HTTP/1.1[0m" 304 -
-2025-08-18 16:31:45,807 [INFO] 127.0.0.1 - - [18/Aug/2025 16:31:45] "[36mGET /static/js/main.js HTTP/1.1[0m" 304 -
-2025-08-18 16:31:46,002 [INFO] 127.0.0.1 - - [18/Aug/2025 16:31:46] "[36mGET /static/css/style.css HTTP/1.1[0m" 304 -
-2025-08-18 16:32:17,741 [INFO] 127.0.0.1 - - [18/Aug/2025 16:32:17] "[35m[1mPOST /signup HTTP/1.1[0m" 500 -
-2025-08-18 16:32:17,763 [INFO] 127.0.0.1 - - [18/Aug/2025 16:32:17] "[36mGET /signup?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1[0m" 304 -
-2025-08-18 16:32:17,765 [INFO] 127.0.0.1 - - [18/Aug/2025 16:32:17] "[36mGET /signup?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1[0m" 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] "[36mGET /signup?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1[0m" 304 -
-2025-08-18 16:32:17,822 [INFO] 127.0.0.1 - - [18/Aug/2025 16:32:17] "[36mGET /signup?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1[0m" 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 = `
-
- `;
-
- 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
-
-
-
-
-
-
-
Audit Log
-
-
-
-
-
-
-
-
-
- | Timestamp |
- User |
- Action |
- Details |
-
-
-
- {% for log in audit_logs %}
-
- | {{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} |
- {{ log.username }} |
- {{ log.action }} |
- {{ log.details }} |
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
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 %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
This will create a backup of your entire server configuration, including:
-
- - Server settings
- - User configurations
- - Security settings
- - Protocol configurations
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% 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 %}
-
- {{ message }}
-
-
- {% 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
-
-
-
-
-
-
- | Device |
- IP Address |
- Location |
- Connected Since |
- Status |
- Actions |
-
-
-
- {% for session in active_sessions %}
-
- |
-
-
- {{ session.device_name }}
-
- |
- {{ session.ip_address }} |
- {{ session.location }} |
- {{ session.connected_since }} |
-
-
- {{ session.status|title }}
- |
-
-
- |
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
Quick Actions
-
-
-
-
-
-
-
-
-
-
System Status
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %}
-
-{% block extra_js %}
-
-
-
-
-{% endblock %}
-
-
-
-
-
Data Download
-
0 B
-
-
-
-
-
-
-
-
Connected Since
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %}
-
-{% block scripts %}
-
-{% endblock %}
-
-
-
-
-
-
-
-
VPN Configuration
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Quick Setup Guide
-
- - Download the Outline Client for your device
- - Copy your access key from this dashboard
- - Open the Outline Client and paste your access key
- - Click "Connect" to start using the VPN
-
-
-
-
-
-
-{% 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.
-
-
-
-
-
-{% 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
-
-
-
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 %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% if not current_user.two_factor_enabled %}
-
-

-
Scan this QR code with your authenticator app
-
or enter this code manually: {{ setup_code }}
-
-
- {% else %}
-
-
-
Two-Factor Authentication is Enabled
-
-
- {% endif %}
-
-
-
-
-{% 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 = `
-
- `;
-
- 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
-
-
-
-
-
-
-
Audit Log
-
-
-
-
-
-
-
-
-
- | Timestamp |
- User |
- Action |
- Details |
-
-
-
- {% for log in audit_logs %}
-
- | {{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} |
- {{ log.username }} |
- {{ log.action }} |
- {{ log.details }} |
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
-
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 %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
This will create a backup of your entire server configuration, including:
-
- - Server settings
- - User configurations
- - Security settings
- - Protocol configurations
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% 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 %}
-
- {{ message }}
-
-
- {% 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
-
-
-
-
-
-
- | Device |
- IP Address |
- Location |
- Connected Since |
- Status |
- Actions |
-
-
-
- {% for session in active_sessions %}
-
- |
-
-
- {{ session.device_name }}
-
- |
- {{ session.ip_address }} |
- {{ session.location }} |
- {{ session.connected_since }} |
-
-
- {{ session.status|title }}
- |
-
-
- |
-
- {% endfor %}
-
-
-
-
-
-
-
-
-
-
Quick Actions
-
-
-
-
-
-
-
-
-
-
System Status
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %}
-
-{% block extra_js %}
-
-
-
-
-{% endblock %}
-
-
-
-
-
Data Download
-
0 B
-
-
-
-
-
-
-
-
Connected Since
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %}
-
-{% block scripts %}
-
-{% endblock %}
-
-
-
-
-
-
-
-
VPN Configuration
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Quick Setup Guide
-
- - Download the Outline Client for your device
- - Copy your access key from this dashboard
- - Open the Outline Client and paste your access key
- - Click "Connect" to start using the VPN
-
-
-
-
-
-
-{% 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.
-
-
-
-
-
-{% 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
-
-
-
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 %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% if not current_user.two_factor_enabled %}
-
-

-
Scan this QR code with your authenticator app
-
or enter this code manually: {{ setup_code }}
-
-
- {% else %}
-
-
-
Two-Factor Authentication is Enabled
-
-
- {% endif %}
-
-
-
-
-{% 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 %}