Files
firegex-traffic-viewer/backend/app.py

230 lines
7.0 KiB
Python
Raw Normal View History

import uvicorn
import secrets
import utils
import os
import asyncio
import logging
from fastapi import FastAPI, HTTPException, Depends, APIRouter
2022-06-28 21:49:03 +02:00
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
2022-07-21 01:02:46 +02:00
from jose import jwt
2022-06-28 21:49:03 +02:00
from passlib.context import CryptContext
from utils.sqlite import SQLite
from utils import API_VERSION, FIREGEX_PORT, FIREGEX_HOST, JWT_ALGORITHM, get_interfaces, socketio_emit, DEBUG, SysctlManager, NORELOAD
2022-07-21 01:02:46 +02:00
from utils.loader import frontend_deploy, load_routers
from utils.models import ChangePasswordModel, IpInterface, PasswordChangeForm, PasswordForm, ResetRequest, StatusModel, StatusMessageModel
from contextlib import asynccontextmanager
2024-10-13 01:50:52 +02:00
from fastapi.middleware.cors import CORSMiddleware
import socketio
2025-02-25 23:53:04 +01:00
from socketio.exceptions import ConnectionRefusedError
2022-06-28 13:26:06 +02:00
2022-06-12 19:16:25 +02:00
# DB init
2022-06-28 16:02:52 +02:00
db = SQLite('db/firegex.db')
2023-04-12 23:53:43 +02:00
sysctl = SysctlManager({
"net.ipv4.conf.all.forwarding": True,
"net.ipv6.conf.all.forwarding": True,
"net.ipv4.conf.all.route_localnet": True,
"net.ipv4.ip_forward": True
})
2022-06-28 21:49:03 +02:00
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/login", auto_error=False)
crypto = CryptContext(schemes=["bcrypt"], deprecated="auto")
@asynccontextmanager
async def lifespan(app):
await startup_main()
yield
await shutdown_main()
2025-02-02 22:27:12 +01:00
app = FastAPI(
debug=DEBUG,
redoc_url=None,
lifespan=lifespan,
docs_url="/api/docs",
title="Firegex API",
version=API_VERSION,
)
2022-06-12 19:16:25 +02:00
2024-10-13 01:50:52 +02:00
if DEBUG:
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
utils.socketio = socketio.AsyncServer(
async_mode="asgi",
cors_allowed_origins=[],
transports=["websocket"]
)
sio_app = socketio.ASGIApp(utils.socketio, socketio_path="/sock/socket.io", other_asgi_app=app)
app.mount("/sock", sio_app)
2022-07-12 20:18:54 +02:00
def APP_STATUS(): return "init" if db.get("password") is None else "run"
def JWT_SECRET(): return db.get("secret")
def set_psw(psw: str):
hash_psw = crypto.hash(psw)
db.put("password",hash_psw)
2022-06-28 21:49:03 +02:00
def create_access_token(data: dict):
to_encode = data.copy()
2022-07-21 01:02:46 +02:00
encoded_jwt = jwt.encode(to_encode, JWT_SECRET(), algorithm=JWT_ALGORITHM)
2022-06-28 21:49:03 +02:00
return encoded_jwt
2022-06-28 13:36:17 +02:00
async def refresh_frontend(additional:list[str]=[]):
await socketio_emit([]+additional)
2022-06-28 21:49:03 +02:00
async def check_login(token: str = Depends(oauth2_scheme)):
if not token:
return False
try:
2022-07-21 01:02:46 +02:00
payload = jwt.decode(token, JWT_SECRET(), algorithms=[JWT_ALGORITHM])
2022-06-28 21:49:03 +02:00
logged_in: bool = payload.get("logged_in")
2022-07-20 21:19:22 +02:00
except Exception:
2022-06-28 21:49:03 +02:00
return False
return logged_in
2025-02-25 23:53:04 +01:00
@utils.socketio.on("connect")
async def sio_connect(sid, environ, auth):
if not auth or not await check_login(auth.get("token")):
raise ConnectionRefusedError("Unauthorized")
utils.sid_list.add(sid)
@utils.socketio.on("disconnect")
async def sio_disconnect(sid):
try:
utils.sid_list.remove(sid)
except KeyError:
pass
async def disconnect_all():
while True:
if len(utils.sid_list) == 0:
break
await utils.socketio.disconnect(utils.sid_list.pop())
@utils.socketio.on("update")
async def updater(): pass
2022-06-28 21:49:03 +02:00
async def is_loggined(auth: bool = Depends(check_login)):
if not auth:
raise HTTPException(
2022-07-21 01:02:46 +02:00
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
2022-06-28 21:49:03 +02:00
return True
2022-06-13 16:12:52 +02:00
2022-07-21 01:02:46 +02:00
api = APIRouter(prefix="/api", dependencies=[Depends(is_loggined)])
2022-07-01 03:59:01 +02:00
@app.get("/api/status", response_model=StatusModel)
async def get_app_status(auth: bool = Depends(check_login)):
"""Get the general status of firegex and your session with firegex"""
2022-06-28 13:26:06 +02:00
return {
"status": APP_STATUS(),
"loggined": auth,
2022-07-21 01:02:46 +02:00
"version": API_VERSION
2022-06-28 13:26:06 +02:00
}
2022-06-13 16:12:52 +02:00
2022-06-28 13:26:06 +02:00
@app.post("/api/login")
2022-06-28 21:49:03 +02:00
async def login_api(form: OAuth2PasswordRequestForm = Depends()):
2022-07-01 03:59:01 +02:00
"""Get a login token to use the firegex api"""
if APP_STATUS() != "run":
raise HTTPException(status_code=400)
2022-06-28 13:26:06 +02:00
if form.password == "":
2022-06-13 16:12:52 +02:00
return {"status":"Cannot insert an empty password!"}
2022-06-28 13:26:06 +02:00
await asyncio.sleep(0.3) # No bruteforce :)
2022-07-12 20:18:54 +02:00
if crypto.verify(form.password, db.get("password")):
2022-06-28 21:49:03 +02:00
return {"access_token": create_access_token({"logged_in": True}), "token_type": "bearer"}
raise HTTPException(406,"Wrong password!")
2022-06-28 13:26:06 +02:00
2025-02-25 23:53:04 +01:00
2022-07-21 01:02:46 +02:00
@app.post('/api/set-password', response_model=ChangePasswordModel)
async def set_password(form: PasswordForm):
"""Set the password of firegex"""
if APP_STATUS() != "init":
raise HTTPException(status_code=400)
2022-06-28 13:26:06 +02:00
if form.password == "":
2022-06-13 16:12:52 +02:00
return {"status":"Cannot insert an empty password!"}
set_psw(form.password)
2022-07-08 15:13:46 +02:00
await refresh_frontend()
2022-06-28 21:49:03 +02:00
return {"status":"ok", "access_token": create_access_token({"logged_in": True})}
2022-06-13 16:12:52 +02:00
2022-07-21 01:02:46 +02:00
@api.post('/change-password', response_model=ChangePasswordModel)
async def change_password(form: PasswordChangeForm):
"""Change the password of firegex"""
if APP_STATUS() != "run":
raise HTTPException(status_code=400)
2022-06-13 16:12:52 +02:00
2022-06-28 13:26:06 +02:00
if form.password == "":
2022-06-13 16:12:52 +02:00
return {"status":"Cannot insert an empty password!"}
2022-07-21 01:02:46 +02:00
if form.expire:
db.put("secret", secrets.token_hex(32))
2025-02-25 23:53:04 +01:00
await disconnect_all()
2022-07-21 01:02:46 +02:00
set_psw(form.password)
2022-07-08 15:13:46 +02:00
await refresh_frontend()
2022-06-28 21:49:03 +02:00
return {"status":"ok", "access_token": create_access_token({"logged_in": True})}
2022-06-13 16:12:52 +02:00
2022-06-15 08:47:13 +02:00
2023-09-22 20:46:50 +02:00
@api.get('/interfaces', response_model=list[IpInterface])
2022-07-21 01:02:46 +02:00
async def get_ip_interfaces():
"""Get a list of ip and ip6 interfaces"""
return get_interfaces()
2022-06-12 19:16:25 +02:00
2022-07-21 01:02:46 +02:00
#Routers Loader
reset, startup, shutdown = load_routers(api)
2022-07-10 15:05:56 +02:00
async def startup_main():
2022-07-21 01:02:46 +02:00
db.init()
if os.getenv("HEX_SET_PSW"):
set_psw(bytes.fromhex(os.getenv("HEX_SET_PSW")).decode())
try:
sysctl.set()
except Exception as e:
logging.error(f"Error setting sysctls: {e}")
2022-07-21 01:02:46 +02:00
await startup()
if not JWT_SECRET():
db.put("secret", secrets.token_hex(32))
2022-07-08 15:13:46 +02:00
await refresh_frontend()
async def shutdown_main():
2022-07-21 01:02:46 +02:00
await shutdown()
2023-04-12 23:53:43 +02:00
sysctl.reset()
2022-07-21 01:02:46 +02:00
db.disconnect()
2022-07-20 21:19:22 +02:00
2022-07-21 01:02:46 +02:00
@api.post('/reset', response_model=StatusMessageModel)
async def reset_firegex(form: ResetRequest):
2022-07-20 21:19:22 +02:00
"""Reset firegex nftables rules and optionally all the database"""
if form.delete:
db.delete()
db.init()
db.put("secret", secrets.token_hex(32))
try:
sysctl.set()
except Exception as e:
logging.error(f"Error setting sysctls: {e}")
2022-07-21 01:02:46 +02:00
await reset(form)
2022-07-20 21:19:22 +02:00
await refresh_frontend()
return {'status': 'ok'}
2022-07-21 01:02:46 +02:00
app.include_router(api)
frontend_deploy(app)
2022-06-12 19:16:25 +02:00
if __name__ == '__main__':
2022-06-28 13:26:06 +02:00
# os.environ {PORT = Backend Port (Main Port), F_PORT = Frontend Port}
2022-06-30 16:00:58 +02:00
os.chdir(os.path.dirname(os.path.realpath(__file__)))
2022-06-28 13:26:06 +02:00
uvicorn.run(
"app:app",
host=FIREGEX_HOST,
2022-07-21 01:02:46 +02:00
port=FIREGEX_PORT,
2025-02-25 23:53:04 +01:00
reload=DEBUG and not NORELOAD,
access_log=True,
workers=1, # Firewall module can't be replicated in multiple workers
# Later the firewall module will be moved to a separate process
# The webserver will communicate using redis (redis is also needed for websockets)
2022-06-28 13:26:06 +02:00
)