Files
bouncer/src/bouncer/__main__.py
T
user 2f40f5e508 feat: add CertFP authentication with SASL EXTERNAL
Per-network, per-nick client certificates (EC P-256, self-signed,
10-year validity) stored as combined PEM files. Authentication
cascade: SASL EXTERNAL > SASL PLAIN > NickServ IDENTIFY.

New commands: GENCERT, CERTFP, DELCERT. GENCERT auto-registers
the fingerprint with NickServ CERT ADD when the network is connected.

Includes email verification module for NickServ registration and
expanded NickServ interaction (IDENTIFY, REGISTER, VERIFY).
2026-02-21 01:15:25 +01:00

104 lines
2.7 KiB
Python

"""Entry point for bouncer."""
from __future__ import annotations
import asyncio
import logging
import signal
import sys
import time
from pathlib import Path
from bouncer import commands
from bouncer.backlog import Backlog
from bouncer.cli import parse_args
from bouncer.config import load
from bouncer.router import Router
from bouncer.server import start
log = logging.getLogger("bouncer")
def _setup_logging(verbose: bool) -> None:
level = logging.DEBUG if verbose else logging.INFO
fmt = "\033[2m%(asctime)s\033[0m %(levelname)-5s \033[38;5;110m%(name)s\033[0m %(message)s"
datefmt = "%H:%M:%S"
logging.basicConfig(level=level, format=fmt, datefmt=datefmt, force=True)
# Also log to file for container environments
fh = logging.FileHandler("/data/bouncer.log")
fh.setLevel(level)
fh.setFormatter(logging.Formatter("%(asctime)s %(levelname)-5s %(name)s %(message)s", datefmt))
logging.getLogger().addHandler(fh)
async def _run(config_path: Path, verbose: bool) -> None:
_setup_logging(verbose)
cfg = load(config_path)
log.info("loaded config: %d network(s)", len(cfg.networks))
# Data directory alongside config
data_dir = config_path.parent
db_path = data_dir / "bouncer.db"
backlog = Backlog(db_path)
await backlog.open()
commands.STARTUP_TIME = time.time()
commands.CONFIG_PATH = config_path
commands.DATA_DIR = data_dir
router = Router(cfg, backlog, data_dir=data_dir)
await router.start_networks()
server = await start(cfg.bouncer, router)
# Graceful shutdown on SIGINT/SIGTERM
loop = asyncio.get_running_loop()
stop_event = asyncio.Event()
def _signal_handler() -> None:
log.info("shutting down...")
stop_event.set()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, _signal_handler)
await stop_event.wait()
server.close()
await server.wait_closed()
await router.stop_networks()
await backlog.close()
log.info("shutdown complete")
def main() -> None:
"""CLI entry point."""
args = parse_args()
if not args.config.exists():
print(f"error: config not found: {args.config}", file=sys.stderr)
sys.exit(1)
try:
if args.cprofile:
import cProfile
prof = cProfile.Profile()
prof.enable()
try:
asyncio.run(_run(args.config, args.verbose))
finally:
prof.disable()
prof.dump_stats(str(args.cprofile))
print(f"cProfile stats written to {args.cprofile}", file=sys.stderr)
else:
asyncio.run(_run(args.config, args.verbose))
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()