openvpn-endpoint-server/server/app/api/internal.py

364 lines
11 KiB
Python

"""Internal API endpoints for container-to-container communication.
These endpoints are used by OpenVPN containers to fetch their configuration.
They should only be accessible from within the Docker network.
"""
import os
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Response, Query
from sqlalchemy.orm import Session
from ..database import get_db
from ..models.vpn_server import VPNServer, VPNServerStatus
from ..models.vpn_profile import VPNProfile
from ..services.vpn_server_service import VPNServerService
from ..services.vpn_sync_service import VPNSyncService
from ..services.certificate_service import CertificateService
# Log directory (shared volume with OpenVPN container)
LOG_DIR = Path("/var/log/openvpn")
router = APIRouter(prefix="/api/internal", tags=["internal"])
@router.get("/health")
async def health_check():
"""Health check endpoint for container startup."""
return {"status": "healthy"}
@router.get("/vpn-servers/active")
async def get_active_servers(db: Session = Depends(get_db)):
"""Get list of all active and ready VPN servers.
Used by OpenVPN container to discover which servers to start.
"""
servers = db.query(VPNServer).filter(
VPNServer.is_active == True
).all()
return [
{
"id": s.id,
"name": s.name,
"port": s.port,
"protocol": s.protocol.value,
"management_port": s.management_port,
"vpn_network": s.vpn_network,
"vpn_netmask": s.vpn_netmask,
"is_ready": s.is_ready,
"has_ca": s.certificate_authority is not None and s.certificate_authority.is_ready,
"has_cert": s.server_cert is not None and s.server_key is not None
}
for s in servers
]
@router.get("/vpn-servers/{server_id}/config")
async def get_server_config(
server_id: int,
db: Session = Depends(get_db)
):
"""Get OpenVPN server configuration file."""
service = VPNServerService(db)
server = service.get_server_by_id(server_id)
if not server:
raise HTTPException(status_code=404, detail="Server not found")
if not server.server_cert or not server.server_key:
raise HTTPException(status_code=400, detail="Server certificate not generated")
config = service.generate_server_config(server)
# Update server status
server.status = VPNServerStatus.STARTING
db.commit()
return Response(content=config, media_type="text/plain")
@router.get("/vpn-servers/{server_id}/ca")
async def get_server_ca(
server_id: int,
db: Session = Depends(get_db)
):
"""Get CA certificate for a VPN server."""
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Server not found")
if not server.certificate_authority or not server.certificate_authority.ca_cert:
raise HTTPException(status_code=400, detail="CA not available")
return Response(
content=server.certificate_authority.ca_cert,
media_type="application/x-pem-file"
)
@router.get("/vpn-servers/{server_id}/cert")
async def get_server_cert(
server_id: int,
db: Session = Depends(get_db)
):
"""Get server certificate."""
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Server not found")
if not server.server_cert:
raise HTTPException(status_code=400, detail="Server certificate not generated")
return Response(
content=server.server_cert,
media_type="application/x-pem-file"
)
@router.get("/vpn-servers/{server_id}/key")
async def get_server_key(
server_id: int,
db: Session = Depends(get_db)
):
"""Get server private key."""
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Server not found")
if not server.server_key:
raise HTTPException(status_code=400, detail="Server key not generated")
return Response(
content=server.server_key,
media_type="application/x-pem-file"
)
@router.get("/vpn-servers/{server_id}/dh")
async def get_server_dh(
server_id: int,
db: Session = Depends(get_db)
):
"""Get DH parameters."""
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Server not found")
if not server.certificate_authority or not server.certificate_authority.dh_params:
raise HTTPException(status_code=400, detail="DH parameters not available")
return Response(
content=server.certificate_authority.dh_params,
media_type="application/x-pem-file"
)
@router.get("/vpn-servers/{server_id}/ta")
async def get_server_ta(
server_id: int,
db: Session = Depends(get_db)
):
"""Get TLS-Auth key."""
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Server not found")
if not server.ta_key:
raise HTTPException(status_code=400, detail="TA key not available")
return Response(
content=server.ta_key,
media_type="text/plain"
)
@router.get("/vpn-servers/{server_id}/crl")
async def get_server_crl(
server_id: int,
db: Session = Depends(get_db)
):
"""Get Certificate Revocation List."""
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Server not found")
if not server.certificate_authority:
raise HTTPException(status_code=400, detail="CA not available")
cert_service = CertificateService(db)
crl = cert_service.get_crl(server.certificate_authority)
return Response(
content=crl,
media_type="application/x-pem-file"
)
@router.post("/vpn-servers/{server_id}/started")
async def notify_server_started(
server_id: int,
db: Session = Depends(get_db)
):
"""Notify that server has started successfully."""
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Server not found")
server.status = VPNServerStatus.RUNNING
db.commit()
return {"status": "ok"}
@router.post("/vpn-servers/{server_id}/stopped")
async def notify_server_stopped(
server_id: int,
db: Session = Depends(get_db)
):
"""Notify that server has stopped."""
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Server not found")
server.status = VPNServerStatus.STOPPED
db.commit()
return {"status": "ok"}
@router.get("/vpn-servers/{server_id}/logs")
async def get_server_logs(
server_id: int,
lines: int = Query(default=100, le=1000),
db: Session = Depends(get_db)
):
"""Get OpenVPN server log file.
Args:
server_id: VPN server ID
lines: Number of lines to return (max 1000)
"""
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Server not found")
log_file = LOG_DIR / f"server-{server_id}.log"
if not log_file.exists():
return {"lines": [], "message": "Log file not found"}
try:
# Read last N lines
with open(log_file, 'r') as f:
all_lines = f.readlines()
log_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
return {
"server_id": server_id,
"server_name": server.name,
"total_lines": len(all_lines),
"returned_lines": len(log_lines),
"lines": [line.rstrip() for line in log_lines]
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error reading log: {str(e)}")
@router.get("/vpn-servers/{server_id}/logs/raw")
async def get_server_logs_raw(
server_id: int,
lines: int = Query(default=100, le=5000),
db: Session = Depends(get_db)
):
"""Get OpenVPN server log file as plain text."""
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Server not found")
log_file = LOG_DIR / f"server-{server_id}.log"
if not log_file.exists():
return Response(content="Log file not found", media_type="text/plain")
try:
with open(log_file, 'r') as f:
all_lines = f.readlines()
log_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
return Response(
content="".join(log_lines),
media_type="text/plain"
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error reading log: {str(e)}")
@router.get("/logs/supervisord")
async def get_supervisord_logs(
lines: int = Query(default=100, le=1000)
):
"""Get supervisord log file."""
log_file = LOG_DIR / "supervisord.log"
if not log_file.exists():
return {"lines": [], "message": "Supervisord log not found"}
try:
with open(log_file, 'r') as f:
all_lines = f.readlines()
log_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
return {
"total_lines": len(all_lines),
"returned_lines": len(log_lines),
"lines": [line.rstrip() for line in log_lines]
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error reading log: {str(e)}")
@router.get("/debug/sync")
async def debug_sync(db: Session = Depends(get_db)):
"""Debug endpoint to check VPN sync status."""
sync_service = VPNSyncService(db)
vpn_service = VPNServerService(db)
# Get all profiles with their CNs
profiles = db.query(VPNProfile).all()
profile_cns = [{"id": p.id, "name": p.name, "cert_cn": p.cert_cn, "gateway_id": p.gateway_id} for p in profiles]
# Get connected clients from all servers
servers = db.query(VPNServer).filter(VPNServer.is_active == True).all()
connected_clients = []
for server in servers:
try:
clients = vpn_service.get_connected_clients(server)
for client in clients:
client['server_id'] = server.id
client['server_name'] = server.name
connected_clients.append(client)
except Exception as e:
connected_clients.append({"server_id": server.id, "error": str(e)})
# Run sync and get result
sync_result = sync_service.sync_all_connections()
return {
"profiles": profile_cns,
"connected_clients": connected_clients,
"sync_result": sync_result
}