235 lines
7.5 KiB
Python
235 lines
7.5 KiB
Python
"""Nginx Proxy Manager API integration.
|
|
|
|
Creates, updates, and deletes proxy host entries so each customer's NetBird
|
|
dashboard is accessible at ``{subdomain}.{base_domain}`` with automatic
|
|
Let's Encrypt SSL certificates.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any, Optional
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Timeout for NPM API calls (seconds)
|
|
NPM_TIMEOUT = 30
|
|
|
|
|
|
async def test_npm_connection(api_url: str, api_token: str) -> dict[str, Any]:
|
|
"""Test connectivity to the Nginx Proxy Manager API.
|
|
|
|
Args:
|
|
api_url: NPM API base URL (e.g. ``http://npm:81/api``).
|
|
api_token: Bearer token for authentication.
|
|
|
|
Returns:
|
|
Dict with ``ok`` (bool) and ``message`` (str).
|
|
"""
|
|
headers = {"Authorization": f"Bearer {api_token}"}
|
|
try:
|
|
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client:
|
|
resp = await client.get(f"{api_url}/nginx/proxy-hosts", headers=headers)
|
|
if resp.status_code == 200:
|
|
count = len(resp.json())
|
|
return {"ok": True, "message": f"Connected. {count} proxy hosts found."}
|
|
return {
|
|
"ok": False,
|
|
"message": f"NPM returned status {resp.status_code}: {resp.text[:200]}",
|
|
}
|
|
except httpx.ConnectError:
|
|
return {"ok": False, "message": "Connection refused. Is NPM running?"}
|
|
except httpx.TimeoutException:
|
|
return {"ok": False, "message": "Connection timed out."}
|
|
except Exception as exc:
|
|
return {"ok": False, "message": f"Unexpected error: {exc}"}
|
|
|
|
|
|
async def create_proxy_host(
|
|
api_url: str,
|
|
api_token: str,
|
|
domain: str,
|
|
forward_host: str,
|
|
forward_port: int = 80,
|
|
admin_email: str = "",
|
|
subdomain: str = "",
|
|
customer_id: int = 0,
|
|
) -> dict[str, Any]:
|
|
"""Create a proxy host entry in NPM with SSL for a customer.
|
|
|
|
The proxy routes traffic as follows:
|
|
- ``/`` -> dashboard container (port 80)
|
|
- ``/api`` -> management container (port 80)
|
|
- ``/signalexchange.*`` -> signal container (port 80)
|
|
- ``/relay`` -> relay container (port 80)
|
|
|
|
Args:
|
|
api_url: NPM API base URL.
|
|
api_token: Bearer token.
|
|
domain: Full domain (e.g. ``kunde1.example.com``).
|
|
forward_host: Container name for the dashboard.
|
|
forward_port: Port to forward to (default 80).
|
|
admin_email: Email for Let's Encrypt.
|
|
subdomain: Customer subdomain for building container names.
|
|
customer_id: Customer ID for building container names.
|
|
|
|
Returns:
|
|
Dict with ``proxy_id`` on success or ``error`` on failure.
|
|
"""
|
|
headers = {
|
|
"Authorization": f"Bearer {api_token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
# Build advanced Nginx config to route sub-paths to different containers
|
|
mgmt_container = f"netbird-kunde{customer_id}-management"
|
|
signal_container = f"netbird-kunde{customer_id}-signal"
|
|
relay_container = f"netbird-kunde{customer_id}-relay"
|
|
|
|
advanced_config = f"""
|
|
# NetBird Management API
|
|
location /api {{
|
|
proxy_pass http://{mgmt_container}:80;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}}
|
|
|
|
# NetBird Signal (gRPC-Web)
|
|
location /signalexchange. {{
|
|
grpc_pass grpc://{signal_container}:80;
|
|
grpc_set_header Host $host;
|
|
}}
|
|
|
|
# NetBird Relay (WebSocket)
|
|
location /relay {{
|
|
proxy_pass http://{relay_container}:80;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
}}
|
|
"""
|
|
|
|
payload = {
|
|
"domain_names": [domain],
|
|
"forward_scheme": "http",
|
|
"forward_host": forward_host,
|
|
"forward_port": forward_port,
|
|
"certificate_id": 0,
|
|
"ssl_forced": True,
|
|
"hsts_enabled": True,
|
|
"hsts_subdomains": False,
|
|
"http2_support": True,
|
|
"block_exploits": True,
|
|
"allow_websocket_upgrade": True,
|
|
"access_list_id": 0,
|
|
"advanced_config": advanced_config.strip(),
|
|
"meta": {
|
|
"letsencrypt_agree": True,
|
|
"letsencrypt_email": admin_email,
|
|
"dns_challenge": False,
|
|
},
|
|
}
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client:
|
|
resp = await client.post(
|
|
f"{api_url}/nginx/proxy-hosts", json=payload, headers=headers
|
|
)
|
|
if resp.status_code in (200, 201):
|
|
data = resp.json()
|
|
proxy_id = data.get("id")
|
|
logger.info("Created NPM proxy host %s (id=%s)", domain, proxy_id)
|
|
|
|
# Request SSL certificate
|
|
await _request_ssl(client, api_url, headers, proxy_id, domain, admin_email)
|
|
|
|
return {"proxy_id": proxy_id}
|
|
else:
|
|
error_msg = f"NPM returned {resp.status_code}: {resp.text[:300]}"
|
|
logger.error("Failed to create proxy host: %s", error_msg)
|
|
return {"error": error_msg}
|
|
except Exception as exc:
|
|
logger.error("NPM API error: %s", exc)
|
|
return {"error": str(exc)}
|
|
|
|
|
|
async def _request_ssl(
|
|
client: httpx.AsyncClient,
|
|
api_url: str,
|
|
headers: dict,
|
|
proxy_id: int,
|
|
domain: str,
|
|
admin_email: str,
|
|
) -> None:
|
|
"""Request a Let's Encrypt SSL certificate for a proxy host.
|
|
|
|
Args:
|
|
client: httpx client.
|
|
api_url: NPM API base URL.
|
|
headers: Auth headers.
|
|
proxy_id: The proxy host ID.
|
|
domain: The domain to certify.
|
|
admin_email: Contact email for LE.
|
|
"""
|
|
ssl_payload = {
|
|
"domain_names": [domain],
|
|
"meta": {
|
|
"letsencrypt_agree": True,
|
|
"letsencrypt_email": admin_email,
|
|
"dns_challenge": False,
|
|
},
|
|
}
|
|
try:
|
|
resp = await client.post(
|
|
f"{api_url}/nginx/certificates", json=ssl_payload, headers=headers
|
|
)
|
|
if resp.status_code in (200, 201):
|
|
cert_id = resp.json().get("id")
|
|
# Assign certificate to proxy host
|
|
await client.put(
|
|
f"{api_url}/nginx/proxy-hosts/{proxy_id}",
|
|
json={"certificate_id": cert_id},
|
|
headers=headers,
|
|
)
|
|
logger.info("SSL certificate assigned to proxy host %s", proxy_id)
|
|
else:
|
|
logger.warning("SSL request returned %s: %s", resp.status_code, resp.text[:200])
|
|
except Exception as exc:
|
|
logger.warning("SSL certificate request failed: %s", exc)
|
|
|
|
|
|
async def delete_proxy_host(
|
|
api_url: str, api_token: str, proxy_id: int
|
|
) -> bool:
|
|
"""Delete a proxy host from NPM.
|
|
|
|
Args:
|
|
api_url: NPM API base URL.
|
|
api_token: Bearer token.
|
|
proxy_id: The proxy host ID to delete.
|
|
|
|
Returns:
|
|
True on success.
|
|
"""
|
|
headers = {"Authorization": f"Bearer {api_token}"}
|
|
try:
|
|
async with httpx.AsyncClient(timeout=NPM_TIMEOUT) as client:
|
|
resp = await client.delete(
|
|
f"{api_url}/nginx/proxy-hosts/{proxy_id}", headers=headers
|
|
)
|
|
if resp.status_code in (200, 204):
|
|
logger.info("Deleted NPM proxy host %d", proxy_id)
|
|
return True
|
|
logger.warning(
|
|
"Failed to delete proxy host %d: %s %s",
|
|
proxy_id, resp.status_code, resp.text[:200],
|
|
)
|
|
return False
|
|
except Exception as exc:
|
|
logger.error("NPM delete error: %s", exc)
|
|
return False
|