From 2552876ae14a4cf00d803916dd832e632fa12efe Mon Sep 17 00:00:00 2001 From: Elora VPN <125687916+eloravpn@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:02:55 +0330 Subject: [PATCH] Fix SSL configuration in panel settings, Add version api and Support IPV6 (#60) --- install.sh | 51 +++++++++++++++++++++++++++++++-- main.py | 53 +++++++++++++++++++++++++++++++++-- src/__init__.py | 38 ++++++++++++++++++++++--- src/config.py | 1 + src/config_setting/service.py | 8 ++++-- src/config_setting/utils.py | 2 +- src/system/__init__.py | 0 src/system/router.py | 10 +++++++ src/system/version_manager.py | 48 +++++++++++++++++++++++++++++++ src/telegram/__init__.py | 4 +-- 10 files changed, 199 insertions(+), 16 deletions(-) create mode 100644 src/system/__init__.py create mode 100644 src/system/router.py create mode 100644 src/system/version_manager.py diff --git a/install.sh b/install.sh index d13eb15..713bbc2 100644 --- a/install.sh +++ b/install.sh @@ -116,8 +116,11 @@ download_latest_release() { LATEST_RELEASE=$(curl -s https://api.github.com/repos/eloravpn/EloraVPNManager/releases/latest) fi - DOWNLOAD_URL=$(echo $LATEST_RELEASE | jq -r '.assets[0].browser_download_url') - VERSION=$(echo $LATEST_RELEASE | jq -r '.tag_name') + # Extract version and download URL + DOWNLOAD_URL=$(echo "$LATEST_RELEASE" | jq -r '.assets[0].browser_download_url') + VERSION=$(echo "$LATEST_RELEASE" | jq -r '.tag_name') + RELEASE_NAME=$(echo "$LATEST_RELEASE" | jq -r '.name') + RELEASE_DATE=$(echo "$LATEST_RELEASE" | jq -r '.published_at') if [ -z "$DOWNLOAD_URL" ] || [ "$DOWNLOAD_URL" = "null" ]; then error "Could not find release download URL" @@ -147,6 +150,31 @@ download_latest_release() { log "All required files verified" } +# Enhanced version file creation with more metadata +create_version_file() { + log "Creating version information file..." + local version_file="$INSTALL_DIR/version.py" + + # Convert GitHub date format to more readable format + local formatted_date=$(date -d "${RELEASE_DATE}" "+%Y-%m-%d %H:%M:%S UTC" 2>/dev/null || echo "Unknown") + + # Create the version file with more metadata + cat > "$version_file" << EOL +""" +This file is automatically generated during installation/update. +Do not modify manually. +""" + +__version__ = "${VERSION}" +__release_name__ = "${RELEASE_NAME}" +__release_date__ = "${formatted_date}" +__build_date__ = "$(date -u +"%Y-%m-%d %H:%M:%S UTC")" +EOL + + chmod 644 "$version_file" + log "Version information file created successfully with version ${VERSION}" +} + # Check if required tools are available check_download_tools() { log "Checking required tools..." @@ -177,6 +205,20 @@ update_env() { # Create .env file cat > "$env_file" << EOL +############################################################################### +# ELORA VPN CONFIGURATION # +############################################################################### +# +# This .env file allows you to override default configurations defined in: +# /opt/elora-vpn/src/config.py +# +# IMPORTANT NOTES: +# - Configuration changes can also be made via Panel > Settings menu +# - Values set in .env take precedence over config.py defaults +# - Values set in Settings Menu take precedence over this configurations +# - Restart service after modifying this file: systemctl restart elora-vpn +# + SUDO_USERNAME = "${USER_NAME}" SUDO_PASSWORD = "${PASSWORD}" @@ -499,7 +541,10 @@ update_config() { log "Update mode: Preserving existing config.json" # Move config.json back if [ -f "$INSTALL_DIR/static.temp/config.json" ]; then - mv "$INSTALL_DIR/static.temp/config.json" "$INSTALL_DIR/static/config.json" + # Remove Base URL from config.json + local temp_config=$(mktemp) + jq 'del(.BASE_URL)' "$INSTALL_DIR/static.temp/config.json" > "$temp_config" && \ + mv "$temp_config" "$INSTALL_DIR/static/config.json" rm -rf "$INSTALL_DIR/static.temp" fi return diff --git a/main.py b/main.py index d229f0e..d8d6725 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,9 @@ +import ssl +from pathlib import Path +from typing import Optional + import uvicorn -from src import app +from src import logger, app from src import config from src.config import ( @@ -11,6 +15,46 @@ UVICORN_SSL_KEYFILE, ) + +def validate_ssl_files() -> tuple[Optional[str], Optional[str]]: + """ + Validate SSL certificate and key files, supporting both CA-signed and self-signed certificates. + Returns a tuple of (certfile, keyfile) if valid, or (None, None) if invalid or not configured. + """ + if not UVICORN_SSL_CERTFILE or not UVICORN_SSL_KEYFILE: + return None, None + + cert_path = Path(UVICORN_SSL_CERTFILE) + key_path = Path(UVICORN_SSL_KEYFILE) + + # Check if files exist + if not cert_path.exists(): + logger.warning(f"SSL certificate file not found: {UVICORN_SSL_CERTFILE}") + return None, None + if not key_path.exists(): + logger.warning(f"SSL key file not found: {UVICORN_SSL_KEYFILE}") + return None, None + + try: + # Create a context that doesn't validate certificate chain + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + # Try to load the certificate and private key + ssl_context.load_cert_chain(certfile=str(cert_path), keyfile=str(key_path)) + + logger.info( + "SSL configuration validated successfully (self-signed certificates supported)" + ) + return str(cert_path), str(key_path) + except (ssl.SSLError, Exception) as e: + logger.warning( + f"Invalid SSL configuration: {str(e)}. Starting server without SSL." + ) + return None, None + + if __name__ == "__main__": # Do NOT change workers count for now # multi-workers support isn't implemented yet for APScheduler and XRay module @@ -23,14 +67,17 @@ uds_path = UVICORN_UDS if UVICORN_UDS and len(UVICORN_UDS.strip()) > 0 else None + # Validate SSL files - will return None, None if invalid + ssl_cert, ssl_key = validate_ssl_files() + try: uvicorn.run( "main:app", host=("127.0.0.1" if DEBUG else UVICORN_HOST), port=UVICORN_PORT, uds=uds_path, - ssl_certfile=UVICORN_SSL_CERTFILE, - ssl_keyfile=UVICORN_SSL_KEYFILE, + ssl_certfile=ssl_cert, + ssl_keyfile=ssl_key, forwarded_allow_ips="*", workers=1, reload=DEBUG, diff --git a/src/__init__.py b/src/__init__.py index 7c17a13..526d76c 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,3 +1,4 @@ +import json import logging import os import signal @@ -31,6 +32,7 @@ from src.subscription.router import router as subscription_router from src.users.router import router as user_router from src.config_setting.router import router as config_setting_router +from src.system.router import router as system_router from src.users.schemas import UserResponse from starlette.exceptions import HTTPException as StarletteHTTPException @@ -94,10 +96,38 @@ app.include_router(club_user_router, prefix="/api", tags=["ClubUser"]) app.include_router(config_setting_router, prefix="/api", tags=["ConfigSettings"]) -# Check if static folder exists -if os.path.exists(static_path) and os.path.isdir(static_path): - # Mount static files if directory exists - app.mount("/static", StaticFiles(directory=static_path), name="static") +app.include_router(system_router, prefix="/api", tags=["system"]) + + +@app.get("/static/config.json") +async def custom_config(request: Request): + try: + config_path = os.path.join(static_path, "config.json") + if not os.path.exists(config_path): + raise HTTPException(status_code=404, detail="Config file not found") + + with open(config_path, "r") as f: + config_data = json.load(f) + + base_url = config.CUSTOM_BASE_URL + + if base_url is None: + # Get domain and port from request + domain = request.headers.get("host", "localhost:8000") + # Detect if request is using HTTPS + protocol = ( + "https" + if request.headers.get("x-forwarded-proto") == "https" + else "http" + ) + config_data["BASE_URL"] = f"{protocol}://{domain}/api/" + + return JSONResponse(content=config_data) + except json.JSONDecodeError: + raise HTTPException(status_code=500, detail="Invalid config file format") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + # Mount static files for specific paths if os.path.exists(static_path) and os.path.isdir(static_path): diff --git a/src/config.py b/src/config.py index b20894c..e13e52f 100644 --- a/src/config.py +++ b/src/config.py @@ -27,6 +27,7 @@ UVICORN_HOST = get_setting("UVICORN_HOST", default="0.0.0.0") UVICORN_PORT = get_setting("UVICORN_PORT", cast=int, default=8000) +CUSTOM_BASE_URL = get_setting("CUSTOM_BASE_URL", cast=str, default=None) UVICORN_UDS = config("UVICORN_UDS", default=None) UVICORN_SSL_CERTFILE = get_setting("UVICORN_SSL_CERTFILE", default=None) UVICORN_SSL_KEYFILE = get_setting("UVICORN_SSL_KEYFILE", default=None) diff --git a/src/config_setting/service.py b/src/config_setting/service.py index bcaa957..9b1ebd0 100644 --- a/src/config_setting/service.py +++ b/src/config_setting/service.py @@ -18,10 +18,12 @@ def get_setting(db: Session, key: str, cast: Optional[type] = None) -> Optional[ # Query database setting = db.query(ConfigSetting).filter(ConfigSetting.key == key).first() - if setting: + if setting and setting.value is not None and setting.value.strip(): try: if cast: - return cast(setting.value) + value = deserialize_value(setting.value, setting.value_type) + if value is not None: + return cast(value) else: value = deserialize_value(setting.value, setting.value_type) return value @@ -85,7 +87,7 @@ def serialize_value(value: Any) -> str: def deserialize_value(value: str, value_type: str) -> Any: """Deserialize value from storage""" - if value_type == "none" or value == "None": + if value_type == "none" or value.lower() == "none" or value.strip() == "": return None elif value_type == "bool": return value.lower() in ("true", "1", "yes", "on", "t") diff --git a/src/config_setting/utils.py b/src/config_setting/utils.py index ddf548e..d7a4b60 100644 --- a/src/config_setting/utils.py +++ b/src/config_setting/utils.py @@ -25,7 +25,7 @@ def get_config( cast: Optional function to cast the value to a specific type """ try: - value = decouple_config(key, default=None) + value = decouple_config(key, default=default) # Handle cases where value is None or 'None' string if value is None or (isinstance(value, str) and value.lower() == "none"): diff --git a/src/system/__init__.py b/src/system/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/system/router.py b/src/system/router.py new file mode 100644 index 0000000..74972f5 --- /dev/null +++ b/src/system/router.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter +from src.system.version_manager import version_manager + +router = APIRouter() + + +@router.get("/version") +async def get_version(): + """Get application version information.""" + return version_manager.get_version_info() diff --git a/src/system/version_manager.py b/src/system/version_manager.py new file mode 100644 index 0000000..3665f88 --- /dev/null +++ b/src/system/version_manager.py @@ -0,0 +1,48 @@ +import importlib.util +from pathlib import Path + + +class VersionManager: + """Manages application version information.""" + + def __init__(self): + self._version = "Unknown" + self._build_date = "Unknown" + self._load_version() + + def _load_version(self): + """Load version information from version.py file.""" + try: + # Get the absolute path to version.py + base_dir = Path(__file__).parent.parent.parent + version_file = base_dir / "version.py" + + if version_file.exists(): + # Load the version.py module + spec = importlib.util.spec_from_file_location("version", version_file) + version_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(version_module) + + # Get version information + self._version = getattr(version_module, "__version__", "Unknown") + self._build_date = getattr(version_module, "__build_date__", "Unknown") + except Exception as e: + print(f"Warning: Could not load version information: {e}") + + @property + def version(self): + """Get the application version.""" + return self._version + + @property + def build_date(self): + """Get the build date.""" + return self._build_date + + def get_version_info(self): + """Get complete version information.""" + return {"version": self._version, "build_date": self._build_date} + + +# Create a singleton instance +version_manager = VersionManager() diff --git a/src/telegram/__init__.py b/src/telegram/__init__.py index 589f536..dd97f2e 100644 --- a/src/telegram/__init__.py +++ b/src/telegram/__init__.py @@ -18,7 +18,7 @@ if TELEGRAM_API_TOKEN or TELEGRAM_PAYMENT_API_TOKEN: apihelper.proxy = {"http": TELEGRAM_PROXY_URL, "https": TELEGRAM_PROXY_URL} -if TELEGRAM_API_TOKEN: +if TELEGRAM_API_TOKEN is not None: bot = TeleBot(TELEGRAM_API_TOKEN) @app.on_event("startup") @@ -39,7 +39,7 @@ def start_bot(): else: logger.warn("Telegram Bot not set!") -if TELEGRAM_PAYMENT_API_TOKEN: +if TELEGRAM_PAYMENT_API_TOKEN is not None: payment_bot = TeleBot(TELEGRAM_PAYMENT_API_TOKEN) @app.on_event("startup")