Skip to content

Commit

Permalink
Fix SSL configuration in panel settings, Add version api and Support …
Browse files Browse the repository at this point in the history
…IPV6 (#60)
  • Loading branch information
eloravpn authored Dec 13, 2024
1 parent ba5bdea commit 2552876
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 16 deletions.
51 changes: 48 additions & 3 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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..."
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down
53 changes: 50 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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
Expand All @@ -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,
Expand Down
38 changes: 34 additions & 4 deletions src/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import logging
import os
import signal
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 5 additions & 3 deletions src/config_setting/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion src/config_setting/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
Empty file added src/system/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions src/system/router.py
Original file line number Diff line number Diff line change
@@ -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()
48 changes: 48 additions & 0 deletions src/system/version_manager.py
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 2 additions & 2 deletions src/telegram/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down

0 comments on commit 2552876

Please sign in to comment.