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

392 lines
14 KiB
Python
Raw Normal View History

2022-06-15 08:47:13 +02:00
from base64 import b64decode
2022-06-28 21:49:03 +02:00
import sqlite3, uvicorn, sys, secrets, re, os, asyncio, httpx, urllib, websockets
2022-07-01 03:59:01 +02:00
from typing import List, Union
from fastapi import FastAPI, HTTPException, WebSocket, Depends
from pydantic import BaseModel, BaseSettings
2022-06-28 13:26:06 +02:00
from fastapi.responses import FileResponse, StreamingResponse
from utils import *
2022-06-28 21:49:03 +02:00
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
2022-07-08 15:13:46 +02:00
from fastapi_socketio import SocketManager
2022-06-13 16:12:52 +02:00
2022-06-28 13:26:06 +02:00
ON_DOCKER = len(sys.argv) > 1 and sys.argv[1] == "DOCKER"
DEBUG = len(sys.argv) > 1 and sys.argv[1] == "DEBUG"
2022-06-12 19:16:25 +02:00
# DB init
2022-06-28 16:02:52 +02:00
if not os.path.exists("db"): os.mkdir("db")
db = SQLite('db/firegex.db')
2022-06-13 18:44:11 +02:00
conf = KeyValueStorage(db)
firewall = ProxyManager(db)
2022-06-13 16:12:52 +02:00
class Settings(BaseSettings):
JWT_ALGORITHM: str = "HS256"
REACT_BUILD_DIR: str = "../frontend/build/" if not ON_DOCKER else "frontend/"
REACT_HTML_PATH: str = os.path.join(REACT_BUILD_DIR,"index.html")
2022-06-30 17:48:04 +02:00
VERSION = "1.3.0"
settings = Settings()
2022-06-28 21:49:03 +02:00
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/login", auto_error=False)
crypto = CryptContext(schemes=["bcrypt"], deprecated="auto")
2022-07-01 03:59:01 +02:00
app = FastAPI(debug=DEBUG, redoc_url=None)
2022-07-08 15:13:46 +02:00
sio = SocketManager(app, "/sock", socketio_path="")
2022-06-12 19:16:25 +02:00
def APP_STATUS(): return "init" if conf.get("password") is None else "run"
def JWT_SECRET(): return conf.get("secret")
2022-07-08 15:13:46 +02:00
async def refresh_frontend():
await sio.emit("update","Refresh")
@sio.on("update")
async def updater(): pass
2022-06-28 19:38:28 +02:00
@app.on_event("startup")
async def startup_event():
db.init()
2022-07-08 15:13:46 +02:00
await firewall.init(refresh_frontend)
await refresh_frontend()
if not JWT_SECRET(): conf.put("secret", secrets.token_hex(32))
2022-06-28 19:38:28 +02:00
2022-07-08 13:13:30 +02:00
@app.on_event("shutdown")
async def shutdown_event():
await firewall.close()
db.disconnect()
2022-06-28 21:49:03 +02:00
def create_access_token(data: dict):
to_encode = data.copy()
encoded_jwt = jwt.encode(to_encode, JWT_SECRET(), algorithm=settings.JWT_ALGORITHM)
2022-06-28 21:49:03 +02:00
return encoded_jwt
2022-06-28 13:36:17 +02:00
2022-06-28 21:49:03 +02:00
async def check_login(token: str = Depends(oauth2_scheme)):
if not token:
return False
try:
payload = jwt.decode(token, JWT_SECRET(), algorithms=[settings.JWT_ALGORITHM])
2022-06-28 21:49:03 +02:00
logged_in: bool = payload.get("logged_in")
except JWTError:
return False
return logged_in
async def is_loggined(auth: bool = Depends(check_login)):
if not auth:
raise HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return True
2022-06-13 16:12:52 +02:00
2022-07-01 03:59:01 +02:00
class StatusModel(BaseModel):
status: str
loggined: bool
version: str
@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,
"version": settings.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
class PasswordForm(BaseModel):
password: str
2022-06-13 16:12:52 +02:00
2022-06-28 13:26:06 +02:00
class PasswordChangeForm(BaseModel):
password: str
expire: bool
2022-06-13 18:44:11 +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-06-28 21:49:03 +02:00
if crypto.verify(form.password, conf.get("password")):
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
2022-07-01 03:59:01 +02:00
class ChangePasswordModel(BaseModel):
status: str
access_token: Union[str,None]
2022-06-13 16:12:52 +02:00
2022-07-01 03:59:01 +02:00
@app.post('/api/change-password', response_model=ChangePasswordModel)
2022-06-28 21:49:03 +02:00
async def change_password(form: PasswordChangeForm, auth: bool = Depends(is_loggined)):
2022-07-01 03:59:01 +02:00
"""Change the password of firegex"""
if APP_STATUS() != "run": raise HTTPException(status_code=400)
2022-06-13 18:44:11 +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-06-28 13:26:06 +02:00
if form.expire:
conf.put("secret", secrets.token_hex(32))
2022-06-28 13:26:06 +02:00
2022-06-28 21:49:03 +02:00
hash_psw = crypto.hash(form.password)
conf.put("password",hash_psw)
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-01 03:59:01 +02:00
@app.post('/api/set-password', response_model=ChangePasswordModel)
2022-06-28 21:49:03 +02:00
async def set_password(form: PasswordForm):
2022-07-01 03:59:01 +02:00
"""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!"}
2022-06-28 21:49:03 +02:00
hash_psw = crypto.hash(form.password)
conf.put("password",hash_psw)
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-01 03:59:01 +02:00
class GeneralStatModel(BaseModel):
closed:int
regexes: int
services: int
@app.get('/api/general-stats', response_model=GeneralStatModel)
2022-06-28 21:49:03 +02:00
async def get_general_stats(auth: bool = Depends(is_loggined)):
2022-07-01 03:59:01 +02:00
"""Get firegex general status about services"""
2022-06-28 13:26:06 +02:00
return db.query("""
SELECT
(SELECT COALESCE(SUM(blocked_packets),0) FROM regexes) closed,
(SELECT COUNT(*) FROM regexes) regexes,
(SELECT COUNT(*) FROM services) services
""")[0]
2022-07-01 03:59:01 +02:00
class ServiceModel(BaseModel):
status: str
2022-07-07 21:56:34 +02:00
port: int
2022-07-01 03:59:01 +02:00
name: str
n_regex: int
n_packets: int
@app.get('/api/services', response_model=List[ServiceModel])
async def get_service_list(auth: bool = Depends(is_loggined)):
"""Get the list of existent firegex services"""
2022-06-28 13:26:06 +02:00
return db.query("""
SELECT
s.status status,
2022-07-07 21:56:34 +02:00
s.port port,
2022-06-28 13:26:06 +02:00
s.name name,
COUNT(r.regex_id) n_regex,
2022-06-28 13:26:06 +02:00
COALESCE(SUM(r.blocked_packets),0) n_packets
2022-07-07 21:56:34 +02:00
FROM services s LEFT JOIN regexes r ON r.service_port = s.port
GROUP BY s.port;
2022-06-28 13:26:06 +02:00
""")
2022-07-07 21:56:34 +02:00
@app.get('/api/service/{service_port}', response_model=ServiceModel)
async def get_service_by_id(service_port: int, auth: bool = Depends(is_loggined)):
2022-07-01 03:59:01 +02:00
"""Get info about a specific service using his id"""
2022-06-28 13:26:06 +02:00
res = db.query("""
SELECT
s.status status,
2022-07-07 21:56:34 +02:00
s.port port,
2022-06-28 13:26:06 +02:00
s.name name,
COUNT(r.regex_id) n_regex,
2022-06-28 13:26:06 +02:00
COALESCE(SUM(r.blocked_packets),0) n_packets
2022-07-07 21:56:34 +02:00
FROM services s LEFT JOIN regexes r ON r.service_port = s.port WHERE s.port = ?
GROUP BY s.port;
""", service_port)
2022-06-28 13:26:06 +02:00
if len(res) == 0: raise HTTPException(status_code=400, detail="This service does not exists!")
return res[0]
2022-07-01 03:59:01 +02:00
class StatusMessageModel(BaseModel):
status:str
2022-07-07 21:56:34 +02:00
@app.get('/api/service/{service_port}/stop', response_model=StatusMessageModel)
async def service_stop(service_port: int, auth: bool = Depends(is_loggined)):
"""Request the stop of a specific service"""
await firewall.get(service_port).next(STATUS.STOP)
2022-07-08 15:13:46 +02:00
await refresh_frontend()
2022-06-13 18:44:11 +02:00
return {'status': 'ok'}
2022-06-12 19:16:25 +02:00
2022-07-07 21:56:34 +02:00
@app.get('/api/service/{service_port}/start', response_model=StatusMessageModel)
async def service_start(service_port: int, auth: bool = Depends(is_loggined)):
2022-07-01 03:59:01 +02:00
"""Request the start of a specific service"""
2022-07-07 21:56:34 +02:00
await firewall.get(service_port).next(STATUS.ACTIVE)
2022-07-08 15:13:46 +02:00
await refresh_frontend()
2022-06-13 18:44:11 +02:00
return {'status': 'ok'}
2022-06-12 19:16:25 +02:00
2022-07-07 21:56:34 +02:00
@app.get('/api/service/{service_port}/delete', response_model=StatusMessageModel)
async def service_delete(service_port: int, auth: bool = Depends(is_loggined)):
2022-07-01 03:59:01 +02:00
"""Request the deletion of a specific service"""
2022-07-07 21:56:34 +02:00
db.query('DELETE FROM services WHERE port = ?;', service_port)
db.query('DELETE FROM regexes WHERE service_port = ?;', service_port)
await firewall.remove(service_port)
2022-07-08 15:13:46 +02:00
await refresh_frontend()
2022-06-30 17:48:04 +02:00
return {'status': 'ok'}
2022-07-08 13:13:30 +02:00
class RenameForm(BaseModel):
name:str
@app.post('/api/service/{service_port}/rename', response_model=StatusMessageModel)
async def service_rename(service_port: int, form: RenameForm, auth: bool = Depends(is_loggined)):
"""Request to change the name of a specific service"""
if not form.name: return {'status': 'The name cannot be empty!'}
db.query('UPDATE services SET name=? WHERE port = ?;', form.name, service_port)
2022-07-08 15:13:46 +02:00
await refresh_frontend()
2022-07-08 13:13:30 +02:00
return {'status': 'ok'}
2022-07-01 03:59:01 +02:00
class RegexModel(BaseModel):
regex:str
mode:str
id:int
2022-07-07 21:56:34 +02:00
service_port:int
2022-07-01 03:59:01 +02:00
is_blacklist: bool
n_packets:int
is_case_sensitive:bool
active:bool
2022-06-30 17:48:04 +02:00
2022-07-07 21:56:34 +02:00
@app.get('/api/service/{service_port}/regexes', response_model=List[RegexModel])
async def get_service_regexe_list(service_port: int, auth: bool = Depends(is_loggined)):
2022-07-01 03:59:01 +02:00
"""Get the list of the regexes of a service"""
2022-06-28 13:26:06 +02:00
return db.query("""
SELECT
2022-07-07 21:56:34 +02:00
regex, mode, regex_id `id`, service_port, is_blacklist,
2022-06-30 15:58:03 +02:00
blocked_packets n_packets, is_case_sensitive, active
2022-07-07 21:56:34 +02:00
FROM regexes WHERE service_port = ?;
""", service_port)
2022-06-28 13:26:06 +02:00
2022-07-01 03:59:01 +02:00
@app.get('/api/regex/{regex_id}', response_model=RegexModel)
async def get_regex_by_id(regex_id: int, auth: bool = Depends(is_loggined)):
"""Get regex info using his id"""
2022-06-28 13:26:06 +02:00
res = db.query("""
SELECT
2022-07-07 21:56:34 +02:00
regex, mode, regex_id `id`, service_port, is_blacklist,
2022-06-30 15:58:03 +02:00
blocked_packets n_packets, is_case_sensitive, active
2022-06-28 13:26:06 +02:00
FROM regexes WHERE `id` = ?;
""", regex_id)
if len(res) == 0: raise HTTPException(status_code=400, detail="This regex does not exists!")
return res[0]
2022-07-01 03:59:01 +02:00
@app.get('/api/regex/{regex_id}/delete', response_model=StatusMessageModel)
async def regex_delete(regex_id: int, auth: bool = Depends(is_loggined)):
"""Delete a regex using his id"""
2022-06-28 13:26:06 +02:00
res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id)
if len(res) != 0:
db.query('DELETE FROM regexes WHERE regex_id = ?;', regex_id)
2022-07-07 21:56:34 +02:00
await firewall.get(res[0]["service_port"]).update_filters()
2022-07-08 15:13:46 +02:00
await refresh_frontend()
2022-06-13 18:44:11 +02:00
return {'status': 'ok'}
2022-06-12 19:16:25 +02:00
2022-07-01 03:59:01 +02:00
@app.get('/api/regex/{regex_id}/enable', response_model=StatusMessageModel)
async def regex_enable(regex_id: int, auth: bool = Depends(is_loggined)):
"""Request the enabling of a regex"""
2022-06-30 15:58:03 +02:00
res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id)
if len(res) != 0:
db.query('UPDATE regexes SET active=1 WHERE regex_id = ?;', regex_id)
2022-07-07 21:56:34 +02:00
await firewall.get(res[0]["service_port"]).update_filters()
2022-07-08 15:13:46 +02:00
await refresh_frontend()
2022-06-30 15:58:03 +02:00
return {'status': 'ok'}
2022-07-01 03:59:01 +02:00
@app.get('/api/regex/{regex_id}/disable', response_model=StatusMessageModel)
async def regex_disable(regex_id: int, auth: bool = Depends(is_loggined)):
"""Request the deactivation of a regex"""
2022-06-30 15:58:03 +02:00
res = db.query('SELECT * FROM regexes WHERE regex_id = ?;', regex_id)
if len(res) != 0:
db.query('UPDATE regexes SET active=0 WHERE regex_id = ?;', regex_id)
2022-07-07 21:56:34 +02:00
await firewall.get(res[0]["service_port"]).update_filters()
2022-07-08 15:13:46 +02:00
await refresh_frontend()
2022-06-30 15:58:03 +02:00
return {'status': 'ok'}
2022-06-28 13:26:06 +02:00
class RegexAddForm(BaseModel):
2022-07-07 21:56:34 +02:00
service_port: int
2022-06-28 13:26:06 +02:00
regex: str
mode: str
2022-07-01 02:29:28 +02:00
active: Union[bool,None]
2022-06-28 13:26:06 +02:00
is_blacklist: bool
is_case_sensitive: bool
2022-07-01 03:59:01 +02:00
@app.post('/api/regexes/add', response_model=StatusMessageModel)
async def add_new_regex(form: RegexAddForm, auth: bool = Depends(is_loggined)):
"""Add a new regex"""
2022-06-15 08:47:13 +02:00
try:
2022-06-28 13:26:06 +02:00
re.compile(b64decode(form.regex))
2022-06-15 08:47:13 +02:00
except Exception:
return {"status":"Invalid regex"}
try:
2022-07-07 21:56:34 +02:00
db.query("INSERT INTO regexes (service_port, regex, is_blacklist, mode, is_case_sensitive, active ) VALUES (?, ?, ?, ?, ?, ?);",
form.service_port, form.regex, form.is_blacklist, form.mode, form.is_case_sensitive, True if form.active is None else form.active )
2022-06-15 08:47:13 +02:00
except sqlite3.IntegrityError:
return {'status': 'An identical regex already exists'}
2022-07-07 21:56:34 +02:00
await firewall.get(form.service_port).update_filters()
2022-07-08 15:13:46 +02:00
await refresh_frontend()
2022-06-13 18:44:11 +02:00
return {'status': 'ok'}
2022-06-12 19:16:25 +02:00
2022-06-28 13:26:06 +02:00
class ServiceAddForm(BaseModel):
name: str
port: int
2022-06-12 19:16:25 +02:00
2022-07-07 21:56:34 +02:00
@app.post('/api/services/add', response_model=StatusMessageModel)
2022-07-01 03:59:01 +02:00
async def add_new_service(form: ServiceAddForm, auth: bool = Depends(is_loggined)):
"""Add a new service"""
2022-06-13 10:59:05 +02:00
try:
2022-07-07 21:56:34 +02:00
db.query("INSERT INTO services (name, port, status) VALUES (?, ?, ?)",
form.name, form.port, STATUS.STOP)
2022-06-13 10:59:05 +02:00
except sqlite3.IntegrityError:
2022-06-30 17:48:04 +02:00
return {'status': 'Name or/and ports of the service has been already assigned to another service'}
await firewall.reload()
2022-07-08 15:13:46 +02:00
await refresh_frontend()
2022-06-30 17:48:04 +02:00
2022-07-07 21:56:34 +02:00
return {'status': 'ok'}
2022-06-12 19:16:25 +02:00
2022-06-28 13:26:06 +02:00
async def frontend_debug_proxy(path):
httpc = httpx.AsyncClient()
2022-06-30 15:58:03 +02:00
req = httpc.build_request("GET",urllib.parse.urljoin(f"http://127.0.0.1:{os.getenv('F_PORT','3000')}", path))
2022-06-28 13:26:06 +02:00
resp = await httpc.send(req, stream=True)
return StreamingResponse(resp.aiter_bytes(),status_code=resp.status_code)
async def react_deploy(path):
file_request = os.path.join(settings.REACT_BUILD_DIR, path)
2022-06-28 13:26:06 +02:00
if not os.path.isfile(file_request):
return FileResponse(settings.REACT_HTML_PATH, media_type='text/html')
2022-06-28 13:26:06 +02:00
else:
return FileResponse(file_request)
2022-06-13 10:59:05 +02:00
if DEBUG:
2022-06-28 13:26:06 +02:00
async def forward_websocket(ws_a: WebSocket, ws_b: websockets.WebSocketClientProtocol):
while True:
data = await ws_a.receive_bytes()
await ws_b.send(data)
async def reverse_websocket(ws_a: WebSocket, ws_b: websockets.WebSocketClientProtocol):
while True:
data = await ws_b.recv()
await ws_a.send_text(data)
@app.websocket("/ws")
async def websocket_debug_proxy(ws: WebSocket):
await ws.accept()
2022-06-30 15:58:03 +02:00
async with websockets.connect(f"ws://127.0.0.1:{os.getenv('F_PORT','3000')}/ws") as ws_b_client:
2022-06-28 13:26:06 +02:00
fwd_task = asyncio.create_task(forward_websocket(ws, ws_b_client))
rev_task = asyncio.create_task(reverse_websocket(ws, ws_b_client))
await asyncio.gather(fwd_task, rev_task)
2022-07-01 03:59:01 +02:00
@app.get("/{full_path:path}", include_in_schema=False)
2022-06-28 21:49:03 +02:00
async def catch_all(full_path:str):
2022-06-28 13:26:06 +02:00
if DEBUG:
try:
return await frontend_debug_proxy(full_path)
except Exception:
2022-06-30 15:58:03 +02:00
return {"details":"Frontend not started at "+f"http://127.0.0.1:{os.getenv('F_PORT','3000')}"}
2022-06-28 13:26:06 +02:00
else: return await react_deploy(full_path)
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="0.0.0.0",
port=int(os.getenv("PORT","4444")),
reload=DEBUG,
2022-06-28 21:49:03 +02:00
access_log=DEBUG,
workers=1
2022-06-28 13:26:06 +02:00
)