diff --git a/wg-manager-backend/database/util.py b/wg-manager-backend/database/util.py index 43ad741..db07c7a 100644 --- a/wg-manager-backend/database/util.py +++ b/wg-manager-backend/database/util.py @@ -1,3 +1,4 @@ +import contextlib import os import alembic.command @@ -14,9 +15,11 @@ from loguru import logger def perform_migrations(): logger.info("Performing migrations...") alembic_cfg = Config("alembic.ini") + alembic_cfg.attributes['configure_logger'] = False alembic_cfg.set_main_option('script_location', "migrations") alembic_cfg.set_main_option('sqlalchemy.url', str(engine.url)) + alembic.command.upgrade(alembic_cfg, 'head') logger.info("Migrations done!") diff --git a/wg-manager-backend/db/wireguard.py b/wg-manager-backend/db/wireguard.py index a995240..29591f5 100644 --- a/wg-manager-backend/db/wireguard.py +++ b/wg-manager-backend/db/wireguard.py @@ -11,10 +11,9 @@ import script.wireguard from sqlalchemy.orm import Session from database import models import schemas -import logging +from loguru import logger -_LOGGER = logging.getLogger(__name__) -_LOGGER.setLevel(logging.DEBUG) +from util import WGMHTTPException def start_client(sess: Session, peer: schemas.WGPeer): @@ -161,8 +160,8 @@ def server_add_on_init(sess: Session): # Only add if it does not already exists. server_add(schemas.WGServerAdd(**init_data), sess, start=const.SERVER_INIT_INTERFACE_START) except Exception as e: - _LOGGER.warning("Failed to setup initial server interface with exception:") - _LOGGER.exception(e) + logger.warning("Failed to setup initial server interface with exception:") + logger.exception(e) def server_add(server: schemas.WGServerAdd, sess: Session, start=False): @@ -180,43 +179,54 @@ def server_add(server: schemas.WGServerAdd, sess: Session, start=False): peers = server.peers if server.peers else [] - # Public/Private key - try: + all_interfaces = sess.query(models.WGServer).all() + check_interface_exists = any(map(lambda el: el.interface == server.interface, all_interfaces)) + check_v4_address_exists = any(map(lambda el: el.address == server.address, all_interfaces)) + check_v6_address_exists = any(map(lambda el: el.v6_address == server.v6_address, all_interfaces)) + check_listen_port_exists = any(map(lambda el: el.listen_port == server.listen_port, all_interfaces)) + if check_interface_exists: + raise WGMHTTPException( + status_code=400, + detail=f"There is already a interface with the name: {server.interface}") + + if check_v4_address_exists: + raise WGMHTTPException( + status_code=400, + detail=f"There is already a interface with the IPv4 address: {server.address}") + + if check_v6_address_exists: + raise WGMHTTPException( + status_code=400, + detail=f"There is already a interface with the IPv6 address: {server.v6_address}") + + if check_listen_port_exists: + raise WGMHTTPException( + status_code=400, + detail=f"There is already a interface listening on port: {server.listen_port}") + + if not server.private_key: + keys = script.wireguard.generate_keys() + server.private_key = keys["private_key"] + server.public_key = keys["public_key"] - if sess.query(models.WGServer) \ - .filter( - (models.WGServer.interface == server.interface) | - (models.WGServer.address == server.address) | - (models.WGServer.v6_address == server.v6_address)).count() != 0: - raise HTTPException(status_code=400, - detail="The server interface or ip %s already exists in the database" % server.interface) - - if not server.private_key: - keys = script.wireguard.generate_keys() - server.private_key = keys["private_key"] - server.public_key = keys["public_key"] - - server.configuration = script.wireguard.generate_config(server) - server.peers = [] - server.sync(sess) - - if len(peers) > 0: - server.from_db(sess) - - for schemaPeer in peers: - schemaPeer.server_id = server.id - schemaPeer.configuration = script.wireguard.generate_config(dict( - peer=schemaPeer, - server=server - )) - dbPeer = models.WGPeer(**schemaPeer.dict()) - sess.add(dbPeer) - sess.commit() + server.configuration = script.wireguard.generate_config(server) + server.peers = [] + server.sync(sess) + if len(peers) > 0: server.from_db(sess) - except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) + for schemaPeer in peers: + schemaPeer.server_id = server.id + schemaPeer.configuration = script.wireguard.generate_config(dict( + peer=schemaPeer, + server=server + )) + dbPeer = models.WGPeer(**schemaPeer.dict()) + sess.add(dbPeer) + sess.commit() + + server.from_db(sess) if start and not script.wireguard.is_running(server): script.wireguard.start_interface(server) diff --git a/wg-manager-backend/logger.py b/wg-manager-backend/logger.py new file mode 100644 index 0000000..063b203 --- /dev/null +++ b/wg-manager-backend/logger.py @@ -0,0 +1,21 @@ + +def setup_logging(): + import logging + from loguru import logger + class InterceptHandler(logging.Handler): + def emit(self, record): + # Get corresponding Loguru level if it exists + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + + # Find caller from where originated the logged message + frame, depth = logging.currentframe(), 2 + while frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 + + logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) + + logging.basicConfig(handlers=[InterceptHandler()], level=1) diff --git a/wg-manager-backend/logging.json b/wg-manager-backend/logging.json new file mode 100644 index 0000000..d56479c --- /dev/null +++ b/wg-manager-backend/logging.json @@ -0,0 +1,11 @@ +{ + "logger": { + "path": "./logs", + "filename": "access.log", + "level": "info", + "rotation": "20 days", + "retention": "1 months", + "format": "{level: <8} {time:YYYY-MM-DD HH:mm:ss.SSS} {extra[request_id]} - {name}:{function} - {message}" + + } +} \ No newline at end of file diff --git a/wg-manager-backend/main.py b/wg-manager-backend/main.py index 18d5556..b62e369 100644 --- a/wg-manager-backend/main.py +++ b/wg-manager-backend/main.py @@ -1,8 +1,14 @@ +from logger import setup_logging +setup_logging() + import const +from uvicorn_loguru_integration import run_uvicorn_loguru + import time from starlette.middleware.base import BaseHTTPMiddleware import middleware + from routers.v1 import user, server, peer, wg import script.wireguard_startup import pkg_resources @@ -15,6 +21,7 @@ import database.util app = FastAPI() app.add_middleware(BaseHTTPMiddleware, dispatch=middleware.db_session_middleware) +app.add_middleware(BaseHTTPMiddleware, dispatch=middleware.logging_middleware) app.include_router( user.router, @@ -83,4 +90,13 @@ if __name__ == "__main__": # Configure wireguard script.wireguard_startup.setup_on_start() - uvicorn.run("__main__:app", reload=True) + run_uvicorn_loguru( + uvicorn.Config( + "__main__:app", + host="0.0.0.0", + port=8000, + log_level="warning", + reload=True, + workers=1 + ) + ) diff --git a/wg-manager-backend/middleware.py b/wg-manager-backend/middleware.py index b49a03e..ce13ca5 100644 --- a/wg-manager-backend/middleware.py +++ b/wg-manager-backend/middleware.py @@ -4,6 +4,7 @@ import jwt from fastapi import Depends, HTTPException from fastapi.security import OAuth2PasswordBearer from jwt import PyJWTError +from loguru import logger from passlib.context import CryptContext from sqlalchemy.orm import Session from starlette import status @@ -27,6 +28,12 @@ def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) +async def logging_middleware(request: Request, call_next): + response = await call_next(request) + logger.opt(depth=2).info(f"{request.method} {request.url} - Code: {response.status_code}") + return response + + async def db_session_middleware(request: Request, call_next): response = Response("Internal server error (Database error)", status_code=500) try: diff --git a/wg-manager-backend/migrations/env.py b/wg-manager-backend/migrations/env.py index 5095440..9a72e24 100644 --- a/wg-manager-backend/migrations/env.py +++ b/wg-manager-backend/migrations/env.py @@ -14,7 +14,8 @@ config = context.config # Interpret the config file for Python logging. # This line sets up loggers basically. -fileConfig(config.config_file_name) +if config.attributes.get('configure_logger', True): + fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support diff --git a/wg-manager-backend/requirements.txt b/wg-manager-backend/requirements.txt index 5102d52..970c54a 100644 --- a/wg-manager-backend/requirements.txt +++ b/wg-manager-backend/requirements.txt @@ -13,6 +13,7 @@ sqlalchemy_utils sqlalchemy-migrate requests uvicorn +uvicorn-loguru-integration uvloop httptools qrcode[pil] diff --git a/wg-manager-backend/util.py b/wg-manager-backend/util.py index 120fb53..95c2aff 100644 --- a/wg-manager-backend/util.py +++ b/wg-manager-backend/util.py @@ -1,2 +1,11 @@ +from loguru import logger +from fastapi import HTTPException from jinja2 import Environment, PackageLoader jinja_env = Environment(loader=PackageLoader(__name__, 'templates')) + + +class WGMHTTPException(HTTPException): + + def __init__(self, status_code: int, detail: str = None): + HTTPException.__init__(self, status_code, detail) + logger.opt(depth=1).error(detail) \ No newline at end of file