Restructurated backend

This commit is contained in:
DomySh
2022-07-21 01:02:46 +02:00
parent d4cc2f566c
commit 0ed8bb635e
15 changed files with 523 additions and 409 deletions

3
.gitignore vendored
View File

@@ -13,8 +13,7 @@
/frontend/coverage
/backend/db/
/backend/db/firegex.db
/backend/db/firegex.db-journal
/backend/db/**
/backend/modules/cppqueue
docker-compose.yml

View File

@@ -1,74 +1,42 @@
from base64 import b64decode
import sqlite3, uvicorn, sys, secrets, re
import httpx, websockets, os, asyncio
from typing import List, Union
from fastapi import FastAPI, HTTPException, WebSocket, Depends
from pydantic import BaseModel, BaseSettings
from fastapi.responses import FileResponse, StreamingResponse
import uvicorn, secrets, utils
import os, asyncio
from typing import List
from fastapi import FastAPI, HTTPException, Depends, APIRouter
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from jose import jwt
from passlib.context import CryptContext
from fastapi_socketio import SocketManager
from modules import SQLite, FirewallManager
from modules.firewall import STATUS
from modules import SQLite
from modules.firegex import FiregexTables
from utils import get_interfaces, ip_parse, refactor_name, gen_service_id
ON_DOCKER = len(sys.argv) > 1 and sys.argv[1] == "DOCKER"
DEBUG = len(sys.argv) > 1 and sys.argv[1] == "DEBUG"
from utils import API_VERSION, FIREGEX_PORT, JWT_ALGORITHM, get_interfaces, refresh_frontend, DEBUG
from utils.loader import frontend_deploy, load_routers
from utils.models import ChangePasswordModel, IpInterface, PasswordChangeForm, PasswordForm, ResetRequest, StatusModel, StatusMessageModel
# DB init
if not os.path.exists("db"): os.mkdir("db")
db = SQLite('db/firegex.db')
firewall = FirewallManager(db)
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")
VERSION = "1.5.0"
settings = Settings()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/login", auto_error=False)
crypto = CryptContext(schemes=["bcrypt"], deprecated="auto")
app = FastAPI(debug=DEBUG, redoc_url=None)
sio = SocketManager(app, "/sock", socketio_path="")
utils.socketio = SocketManager(app, "/sock", socketio_path="")
def APP_STATUS(): return "init" if db.get("password") is None else "run"
def JWT_SECRET(): return db.get("secret")
async def refresh_frontend():
await sio.emit("update","Refresh")
@sio.on("update")
@utils.socketio.on("update")
async def updater(): pass
@app.on_event("startup")
async def startup_event():
db.init()
await firewall.init()
await refresh_frontend()
if not JWT_SECRET(): db.put("secret", secrets.token_hex(32))
@app.on_event("shutdown")
async def shutdown_event():
db.backup()
await firewall.close()
db.disconnect()
db.restore()
def create_access_token(data: dict):
to_encode = data.copy()
encoded_jwt = jwt.encode(to_encode, JWT_SECRET(), algorithm=settings.JWT_ALGORITHM)
encoded_jwt = jwt.encode(to_encode, JWT_SECRET(), algorithm=JWT_ALGORITHM)
return encoded_jwt
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])
payload = jwt.decode(token, JWT_SECRET(), algorithms=[JWT_ALGORITHM])
logged_in: bool = payload.get("logged_in")
except Exception:
return False
@@ -77,16 +45,13 @@ async def check_login(token: str = Depends(oauth2_scheme)):
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"},
)
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return True
class StatusModel(BaseModel):
status: str
loggined: bool
version: str
api = APIRouter(prefix="/api", dependencies=[Depends(is_loggined)])
@app.get("/api/status", response_model=StatusModel)
async def get_app_status(auth: bool = Depends(check_login)):
@@ -94,16 +59,9 @@ async def get_app_status(auth: bool = Depends(check_login)):
return {
"status": APP_STATUS(),
"loggined": auth,
"version": settings.VERSION
"version": API_VERSION
}
class PasswordForm(BaseModel):
password: str
class PasswordChangeForm(BaseModel):
password: str
expire: bool
@app.post("/api/login")
async def login_api(form: OAuth2PasswordRequestForm = Depends()):
"""Get a login token to use the firegex api"""
@@ -115,12 +73,19 @@ async def login_api(form: OAuth2PasswordRequestForm = Depends()):
return {"access_token": create_access_token({"logged_in": True}), "token_type": "bearer"}
raise HTTPException(406,"Wrong password!")
class ChangePasswordModel(BaseModel):
status: str
access_token: Union[str,None]
@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)
if form.password == "":
return {"status":"Cannot insert an empty password!"}
hash_psw = crypto.hash(form.password)
db.put("password",hash_psw)
await refresh_frontend()
return {"status":"ok", "access_token": create_access_token({"logged_in": True})}
@app.post('/api/change-password', response_model=ChangePasswordModel)
async def change_password(form: PasswordChangeForm, auth: bool = Depends(is_loggined)):
@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)
@@ -135,306 +100,39 @@ async def change_password(form: PasswordChangeForm, auth: bool = Depends(is_logg
return {"status":"ok", "access_token": create_access_token({"logged_in": True})}
@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)
if form.password == "":
return {"status":"Cannot insert an empty password!"}
hash_psw = crypto.hash(form.password)
db.put("password",hash_psw)
await refresh_frontend()
return {"status":"ok", "access_token": create_access_token({"logged_in": True})}
class GeneralStatModel(BaseModel):
closed:int
regexes: int
services: int
@app.get('/api/general-stats', response_model=GeneralStatModel)
async def get_general_stats(auth: bool = Depends(is_loggined)):
"""Get firegex general status about services"""
return db.query("""
SELECT
(SELECT COALESCE(SUM(blocked_packets),0) FROM regexes) closed,
(SELECT COUNT(*) FROM regexes) regexes,
(SELECT COUNT(*) FROM services) services
""")[0]
class ServiceModel(BaseModel):
status: str
service_id: str
port: int
name: str
proto: str
ip_int: 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"""
return db.query("""
SELECT
s.service_id service_id,
s.status status,
s.port port,
s.name name,
s.proto proto,
s.ip_int ip_int,
COUNT(r.regex_id) n_regex,
COALESCE(SUM(r.blocked_packets),0) n_packets
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
GROUP BY s.service_id;
""")
@app.get('/api/service/{service_id}', response_model=ServiceModel)
async def get_service_by_id(service_id: str, auth: bool = Depends(is_loggined)):
"""Get info about a specific service using his id"""
res = db.query("""
SELECT
s.service_id service_id,
s.status status,
s.port port,
s.name name,
s.proto proto,
s.ip_int ip_int,
COUNT(r.regex_id) n_regex,
COALESCE(SUM(r.blocked_packets),0) n_packets
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
WHERE s.service_id = ? GROUP BY s.service_id;
""", service_id)
if len(res) == 0: raise HTTPException(status_code=400, detail="This service does not exists!")
return res[0]
class StatusMessageModel(BaseModel):
status:str
@app.get('/api/service/{service_id}/stop', response_model=StatusMessageModel)
async def service_stop(service_id: str, auth: bool = Depends(is_loggined)):
"""Request the stop of a specific service"""
await firewall.get(service_id).next(STATUS.STOP)
await refresh_frontend()
return {'status': 'ok'}
@app.get('/api/service/{service_id}/start', response_model=StatusMessageModel)
async def service_start(service_id: str, auth: bool = Depends(is_loggined)):
"""Request the start of a specific service"""
await firewall.get(service_id).next(STATUS.ACTIVE)
await refresh_frontend()
return {'status': 'ok'}
@app.get('/api/service/{service_id}/delete', response_model=StatusMessageModel)
async def service_delete(service_id: str, auth: bool = Depends(is_loggined)):
"""Request the deletion of a specific service"""
db.query('DELETE FROM services WHERE service_id = ?;', service_id)
db.query('DELETE FROM regexes WHERE service_id = ?;', service_id)
await firewall.remove(service_id)
await refresh_frontend()
return {'status': 'ok'}
class RenameForm(BaseModel):
name:str
@app.post('/api/service/{service_id}/rename', response_model=StatusMessageModel)
async def service_rename(service_id: str, form: RenameForm, auth: bool = Depends(is_loggined)):
"""Request to change the name of a specific service"""
form.name = refactor_name(form.name)
if not form.name: return {'status': 'The name cannot be empty!'}
try:
db.query('UPDATE services SET name=? WHERE service_id = ?;', form.name, service_id)
except sqlite3.IntegrityError:
return {'status': 'This name is already used'}
await refresh_frontend()
return {'status': 'ok'}
class RegexModel(BaseModel):
regex:str
mode:str
id:int
service_id:str
is_blacklist: bool
n_packets:int
is_case_sensitive:bool
active:bool
@app.get('/api/service/{service_id}/regexes', response_model=List[RegexModel])
async def get_service_regexe_list(service_id: str, auth: bool = Depends(is_loggined)):
"""Get the list of the regexes of a service"""
return db.query("""
SELECT
regex, mode, regex_id `id`, service_id, is_blacklist,
blocked_packets n_packets, is_case_sensitive, active
FROM regexes WHERE service_id = ?;
""", service_id)
@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"""
res = db.query("""
SELECT
regex, mode, regex_id `id`, service_id, is_blacklist,
blocked_packets n_packets, is_case_sensitive, active
FROM regexes WHERE `id` = ?;
""", regex_id)
if len(res) == 0: raise HTTPException(status_code=400, detail="This regex does not exists!")
return res[0]
@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"""
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)
await firewall.get(res[0]["service_id"]).update_filters()
await refresh_frontend()
return {'status': 'ok'}
@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"""
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)
await firewall.get(res[0]["service_id"]).update_filters()
await refresh_frontend()
return {'status': 'ok'}
@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"""
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)
await firewall.get(res[0]["service_id"]).update_filters()
await refresh_frontend()
return {'status': 'ok'}
class RegexAddForm(BaseModel):
service_id: str
regex: str
mode: str
active: Union[bool,None]
is_blacklist: bool
is_case_sensitive: bool
@app.post('/api/regexes/add', response_model=StatusMessageModel)
async def add_new_regex(form: RegexAddForm, auth: bool = Depends(is_loggined)):
"""Add a new regex"""
try:
re.compile(b64decode(form.regex))
except Exception:
return {"status":"Invalid regex"}
try:
db.query("INSERT INTO regexes (service_id, regex, is_blacklist, mode, is_case_sensitive, active ) VALUES (?, ?, ?, ?, ?, ?);",
form.service_id, form.regex, form.is_blacklist, form.mode, form.is_case_sensitive, True if form.active is None else form.active )
except sqlite3.IntegrityError:
return {'status': 'An identical regex already exists'}
await firewall.get(form.service_id).update_filters()
await refresh_frontend()
return {'status': 'ok'}
class ServiceAddForm(BaseModel):
name: str
port: int
proto: str
ip_int: str
class ServiceAddResponse(BaseModel):
status:str
service_id: Union[None,str]
@app.post('/api/services/add', response_model=ServiceAddResponse)
async def add_new_service(form: ServiceAddForm, auth: bool = Depends(is_loggined)):
"""Add a new service"""
try:
form.ip_int = ip_parse(form.ip_int)
except ValueError:
return {"status":"Invalid address"}
if form.proto not in ["tcp", "udp"]:
return {"status":"Invalid protocol"}
srv_id = None
try:
srv_id = gen_service_id(db)
db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int) VALUES (?, ?, ?, ?, ?, ?)",
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int)
except sqlite3.IntegrityError:
return {'status': 'This type of service already exists'}
await firewall.reload()
await refresh_frontend()
return {'status': 'ok', 'service_id': srv_id}
class IpInterface(BaseModel):
addr: str
name: str
@app.get('/api/interfaces', response_model=List[IpInterface])
async def get_ip_interfaces(auth: bool = Depends(is_loggined)):
@api.get('/interfaces', response_model=List[IpInterface])
async def get_ip_interfaces():
"""Get a list of ip and ip6 interfaces"""
return get_interfaces()
class ResetRequest(BaseModel):
delete:bool
#Routers Loader
reset, startup, shutdown = load_routers(api)
@app.post('/api/reset', response_model=StatusMessageModel)
async def reset_firegex(form: ResetRequest, auth: bool = Depends(is_loggined)):
@app.on_event("startup")
async def startup_event():
db.init()
await startup()
if not JWT_SECRET(): db.put("secret", secrets.token_hex(32))
await refresh_frontend()
@app.on_event("shutdown")
async def shutdown_event():
await shutdown()
db.disconnect()
@api.post('/reset', response_model=StatusMessageModel)
async def reset_firegex(form: ResetRequest):
"""Reset firegex nftables rules and optionally all the database"""
if not form.delete:
db.backup()
await firewall.close()
FiregexTables().reset()
if form.delete:
db.delete()
db.init()
db.put("secret", secrets.token_hex(32))
else:
db.restore()
await firewall.init()
await reset(form)
await refresh_frontend()
return {'status': 'ok'}
async def frontend_debug_proxy(path):
httpc = httpx.AsyncClient()
req = httpc.build_request("GET",f"http://127.0.0.1:{os.getenv('F_PORT','3000')}/"+path)
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)
if not os.path.isfile(file_request):
return FileResponse(settings.REACT_HTML_PATH, media_type='text/html')
else:
return FileResponse(file_request)
if DEBUG:
async def forward_websocket(ws_a, ws_b):
while True:
data = await ws_a.receive_bytes()
await ws_b.send(data)
async def reverse_websocket(ws_a, ws_b):
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()
async with websockets.connect(f"ws://127.0.0.1:{os.getenv('F_PORT','3000')}/ws") as ws_b_client:
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)
@app.get("/{full_path:path}", include_in_schema=False)
async def catch_all(full_path:str):
if DEBUG:
try:
return await frontend_debug_proxy(full_path)
except Exception:
return {"details":"Frontend not started at "+f"http://127.0.0.1:{os.getenv('F_PORT','3000')}"}
else: return await react_deploy(full_path)
app.include_router(api)
frontend_deploy(app)
if __name__ == '__main__':
# os.environ {PORT = Backend Port (Main Port), F_PORT = Frontend Port}
@@ -442,7 +140,7 @@ if __name__ == '__main__':
uvicorn.run(
"app:app",
host="0.0.0.0",
port=int(os.getenv("PORT","4444")),
port=FIREGEX_PORT,
reload=DEBUG,
access_log=DEBUG,
workers=1

View File

@@ -1,5 +1,5 @@
from typing import Dict, List, Set
from utils import ip_parse, ip_family
from utils import ip_parse, ip_family, run_func
from modules.sqlite import Service
import re, os, asyncio
import traceback, nftables
@@ -182,8 +182,7 @@ class RegexFilter:
async def update(self):
if self.update_func:
if asyncio.iscoroutinefunction(self.update_func): await self.update_func(self)
else: self.update_func(self)
await run_func(self.update_func, self)
class FiregexInterceptor:

View File

@@ -4,42 +4,20 @@ from hashlib import md5
import base64
class SQLite():
def __init__(self, db_name: str) -> None:
def __init__(self, db_name: str, schema:dict = None) -> None:
self.conn: Union[None, sqlite3.Connection] = None
self.cur = None
self.db_name = db_name
self.__backup = None
self.schema = {
'services': {
'service_id': 'VARCHAR(100) PRIMARY KEY',
'status': 'VARCHAR(100) NOT NULL',
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
'name': 'VARCHAR(100) NOT NULL UNIQUE',
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))',
'ip_int': 'VARCHAR(100) NOT NULL',
},
'regexes': {
'regex': 'TEXT NOT NULL',
'mode': 'VARCHAR(1) NOT NULL',
'service_id': 'VARCHAR(100) NOT NULL',
'is_blacklist': 'BOOLEAN NOT NULL CHECK (is_blacklist IN (0, 1))',
'blocked_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0',
'regex_id': 'INTEGER PRIMARY KEY',
'is_case_sensitive' : 'BOOLEAN NOT NULL CHECK (is_case_sensitive IN (0, 1))',
'active' : 'BOOLEAN NOT NULL CHECK (active IN (0, 1)) DEFAULT 1',
'FOREIGN KEY (service_id)':'REFERENCES services (service_id)',
},
'QUERY':[
"CREATE UNIQUE INDEX IF NOT EXISTS unique_services ON services (port, ip_int, proto);",
"CREATE UNIQUE INDEX IF NOT EXISTS unique_regex_service ON regexes (regex,service_id,is_blacklist,mode,is_case_sensitive);"
]
}
self.schema = {} if schema is None else schema
self.DB_VER = md5(json.dumps(self.schema).encode()).hexdigest()
def connect(self) -> None:
try:
self.conn = sqlite3.connect(self.db_name, check_same_thread = False)
except Exception:
path_name = os.path.dirname(self.db_name)
if not os.path.exists(path_name): os.makedirs(path_name)
with open(self.db_name, 'x'): pass
self.conn = sqlite3.connect(self.db_name, check_same_thread = False)
def dict_factory(cursor, row):

View File

281
backend/routers/nfregex.py Normal file
View File

@@ -0,0 +1,281 @@
from base64 import b64decode
import re
import sqlite3
from typing import List, Union
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from modules.firegex import FiregexTables
from modules.firewall import STATUS, FirewallManager
from modules.sqlite import SQLite
from utils import gen_service_id, ip_parse, refactor_name, refresh_frontend
from utils.models import ResetRequest, StatusMessageModel
class GeneralStatModel(BaseModel):
closed:int
regexes: int
services: int
class ServiceModel(BaseModel):
status: str
service_id: str
port: int
name: str
proto: str
ip_int: str
n_regex: int
n_packets: int
class RenameForm(BaseModel):
name:str
class RegexModel(BaseModel):
regex:str
mode:str
id:int
service_id:str
is_blacklist: bool
n_packets:int
is_case_sensitive:bool
active:bool
class RegexAddForm(BaseModel):
service_id: str
regex: str
mode: str
active: Union[bool,None]
is_blacklist: bool
is_case_sensitive: bool
class ServiceAddForm(BaseModel):
name: str
port: int
proto: str
ip_int: str
class ServiceAddResponse(BaseModel):
status:str
service_id: Union[None,str]
app = APIRouter()
async def reset(params: ResetRequest):
if not params.delete:
db.backup()
await firewall.close()
FiregexTables().reset()
if params.delete:
db.delete()
db.init()
else:
db.restore()
await firewall.init()
async def startup():
db.init()
await firewall.init()
async def shutdown():
db.backup()
await firewall.close()
db.disconnect()
db.restore()
db = SQLite('db/nft-regex.db', {
'services': {
'service_id': 'VARCHAR(100) PRIMARY KEY',
'status': 'VARCHAR(100) NOT NULL',
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
'name': 'VARCHAR(100) NOT NULL UNIQUE',
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))',
'ip_int': 'VARCHAR(100) NOT NULL',
},
'regexes': {
'regex': 'TEXT NOT NULL',
'mode': 'VARCHAR(1) NOT NULL',
'service_id': 'VARCHAR(100) NOT NULL',
'is_blacklist': 'BOOLEAN NOT NULL CHECK (is_blacklist IN (0, 1))',
'blocked_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0',
'regex_id': 'INTEGER PRIMARY KEY',
'is_case_sensitive' : 'BOOLEAN NOT NULL CHECK (is_case_sensitive IN (0, 1))',
'active' : 'BOOLEAN NOT NULL CHECK (active IN (0, 1)) DEFAULT 1',
'FOREIGN KEY (service_id)':'REFERENCES services (service_id)',
},
'QUERY':[
"CREATE UNIQUE INDEX IF NOT EXISTS unique_services ON services (port, ip_int, proto);",
"CREATE UNIQUE INDEX IF NOT EXISTS unique_regex_service ON regexes (regex,service_id,is_blacklist,mode,is_case_sensitive);"
]
})
firewall = FirewallManager(db)
@app.get('/stats', response_model=GeneralStatModel)
async def get_general_stats():
"""Get firegex general status about services"""
return db.query("""
SELECT
(SELECT COALESCE(SUM(blocked_packets),0) FROM regexes) closed,
(SELECT COUNT(*) FROM regexes) regexes,
(SELECT COUNT(*) FROM services) services
""")[0]
@app.get('/services', response_model=List[ServiceModel])
async def get_service_list():
"""Get the list of existent firegex services"""
return db.query("""
SELECT
s.service_id service_id,
s.status status,
s.port port,
s.name name,
s.proto proto,
s.ip_int ip_int,
COUNT(r.regex_id) n_regex,
COALESCE(SUM(r.blocked_packets),0) n_packets
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
GROUP BY s.service_id;
""")
@app.get('/service/{service_id}', response_model=ServiceModel)
async def get_service_by_id(service_id: str, ):
"""Get info about a specific service using his id"""
res = db.query("""
SELECT
s.service_id service_id,
s.status status,
s.port port,
s.name name,
s.proto proto,
s.ip_int ip_int,
COUNT(r.regex_id) n_regex,
COALESCE(SUM(r.blocked_packets),0) n_packets
FROM services s LEFT JOIN regexes r ON s.service_id = r.service_id
WHERE s.service_id = ? GROUP BY s.service_id;
""", service_id)
if len(res) == 0: raise HTTPException(status_code=400, detail="This service does not exists!")
return res[0]
@app.get('/service/{service_id}/stop', response_model=StatusMessageModel)
async def service_stop(service_id: str, ):
"""Request the stop of a specific service"""
await firewall.get(service_id).next(STATUS.STOP)
await refresh_frontend()
return {'status': 'ok'}
@app.get('/service/{service_id}/start', response_model=StatusMessageModel)
async def service_start(service_id: str, ):
"""Request the start of a specific service"""
await firewall.get(service_id).next(STATUS.ACTIVE)
await refresh_frontend()
return {'status': 'ok'}
@app.get('/service/{service_id}/delete', response_model=StatusMessageModel)
async def service_delete(service_id: str, ):
"""Request the deletion of a specific service"""
db.query('DELETE FROM services WHERE service_id = ?;', service_id)
db.query('DELETE FROM regexes WHERE service_id = ?;', service_id)
await firewall.remove(service_id)
await refresh_frontend()
return {'status': 'ok'}
@app.post('/service/{service_id}/rename', response_model=StatusMessageModel)
async def service_rename(service_id: str, form: RenameForm, ):
"""Request to change the name of a specific service"""
form.name = refactor_name(form.name)
if not form.name: return {'status': 'The name cannot be empty!'}
try:
db.query('UPDATE services SET name=? WHERE service_id = ?;', form.name, service_id)
except sqlite3.IntegrityError:
return {'status': 'This name is already used'}
await refresh_frontend()
return {'status': 'ok'}
@app.get('/service/{service_id}/regexes', response_model=List[RegexModel])
async def get_service_regexe_list(service_id: str, ):
"""Get the list of the regexes of a service"""
return db.query("""
SELECT
regex, mode, regex_id `id`, service_id, is_blacklist,
blocked_packets n_packets, is_case_sensitive, active
FROM regexes WHERE service_id = ?;
""", service_id)
@app.get('/regex/{regex_id}', response_model=RegexModel)
async def get_regex_by_id(regex_id: int, ):
"""Get regex info using his id"""
res = db.query("""
SELECT
regex, mode, regex_id `id`, service_id, is_blacklist,
blocked_packets n_packets, is_case_sensitive, active
FROM regexes WHERE `id` = ?;
""", regex_id)
if len(res) == 0: raise HTTPException(status_code=400, detail="This regex does not exists!")
return res[0]
@app.get('/regex/{regex_id}/delete', response_model=StatusMessageModel)
async def regex_delete(regex_id: int, ):
"""Delete a regex using his id"""
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)
await firewall.get(res[0]["service_id"]).update_filters()
await refresh_frontend()
return {'status': 'ok'}
@app.get('/regex/{regex_id}/enable', response_model=StatusMessageModel)
async def regex_enable(regex_id: int, ):
"""Request the enabling of a regex"""
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)
await firewall.get(res[0]["service_id"]).update_filters()
await refresh_frontend()
return {'status': 'ok'}
@app.get('/regex/{regex_id}/disable', response_model=StatusMessageModel)
async def regex_disable(regex_id: int, ):
"""Request the deactivation of a regex"""
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)
await firewall.get(res[0]["service_id"]).update_filters()
await refresh_frontend()
return {'status': 'ok'}
@app.post('/regexes/add', response_model=StatusMessageModel)
async def add_new_regex(form: RegexAddForm, ):
"""Add a new regex"""
try:
re.compile(b64decode(form.regex))
except Exception:
return {"status":"Invalid regex"}
try:
db.query("INSERT INTO regexes (service_id, regex, is_blacklist, mode, is_case_sensitive, active ) VALUES (?, ?, ?, ?, ?, ?);",
form.service_id, form.regex, form.is_blacklist, form.mode, form.is_case_sensitive, True if form.active is None else form.active )
except sqlite3.IntegrityError:
return {'status': 'An identical regex already exists'}
await firewall.get(form.service_id).update_filters()
await refresh_frontend()
return {'status': 'ok'}
@app.post('/services/add', response_model=ServiceAddResponse)
async def add_new_service(form: ServiceAddForm, ):
"""Add a new service"""
try:
form.ip_int = ip_parse(form.ip_int)
except ValueError:
return {"status":"Invalid address"}
if form.proto not in ["tcp", "udp"]:
return {"status":"Invalid protocol"}
srv_id = None
try:
srv_id = gen_service_id(db)
db.query("INSERT INTO services (service_id ,name, port, status, proto, ip_int) VALUES (?, ?, ?, ?, ?, ?)",
srv_id, refactor_name(form.name), form.port, STATUS.STOP, form.proto, form.ip_int)
except sqlite3.IntegrityError:
return {'status': 'This type of service already exists'}
await firewall.reload()
await refresh_frontend()
return {'status': 'ok', 'service_id': srv_id}

View File

@@ -1,8 +1,30 @@
import asyncio
from ipaddress import ip_interface
import os, socket, secrets, psutil
import sys
from fastapi_socketio import SocketManager
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
socketio:SocketManager = None
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
ON_DOCKER = len(sys.argv) > 1 and sys.argv[1] == "DOCKER"
DEBUG = len(sys.argv) > 1 and sys.argv[1] == "DEBUG"
FIREGEX_PORT = int(os.getenv("PORT","4444"))
JWT_ALGORITHM: str = "HS256"
API_VERSION = "2.0.0"
async def run_func(func, *args, **kwargs):
if asyncio.iscoroutinefunction(func):
return await func(*args, **kwargs)
else:
return func(*args, **kwargs)
async def refresh_frontend():
await socketio.emit("update","Refresh")
def refactor_name(name:str):
name = name.strip()
while " " in name: name = name.replace(" "," ")
@@ -15,6 +37,11 @@ def gen_service_id(db):
break
return res
def list_files(mypath):
from os import listdir
from os.path import isfile, join
return [f for f in listdir(mypath) if isfile(join(mypath, f))]
def ip_parse(ip:str):
return str(ip_interface(ip).network)

106
backend/utils/loader.py Normal file
View File

@@ -0,0 +1,106 @@
import os, httpx, websockets
from sys import prefix
from typing import Callable, List, Union
from fastapi import APIRouter, WebSocket
import asyncio
from starlette.responses import StreamingResponse
from fastapi.responses import FileResponse
from utils import DEBUG, ON_DOCKER, ROUTERS_DIR, list_files, run_func
from utils.models import ResetRequest
REACT_BUILD_DIR: str = "../frontend/build/" if not ON_DOCKER else "frontend/"
REACT_HTML_PATH: str = os.path.join(REACT_BUILD_DIR,"index.html")
async def frontend_debug_proxy(path):
httpc = httpx.AsyncClient()
req = httpc.build_request("GET",f"http://127.0.0.1:{os.getenv('F_PORT','3000')}/"+path)
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(REACT_BUILD_DIR, path)
if not os.path.isfile(file_request):
return FileResponse(REACT_HTML_PATH, media_type='text/html')
else:
return FileResponse(file_request)
def frontend_deploy(app):
if DEBUG:
async def forward_websocket(ws_a, ws_b):
while True:
data = await ws_a.receive_bytes()
await ws_b.send(data)
async def reverse_websocket(ws_a, ws_b):
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()
async with websockets.connect(f"ws://127.0.0.1:{os.getenv('F_PORT','3000')}/ws") as ws_b_client:
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)
@app.get("/{full_path:path}", include_in_schema=False)
async def catch_all(full_path:str):
if DEBUG:
try:
return await frontend_debug_proxy(full_path)
except Exception:
return {"details":"Frontend not started at "+f"http://127.0.0.1:{os.getenv('F_PORT','3000')}"}
else: return await react_deploy(full_path)
def list_routers():
return [ele[:-3] for ele in list_files(ROUTERS_DIR) if ele != "__init__.py" and " " not in ele and ele.endswith(".py")]
class RouterModule():
router: Union[None, APIRouter]
reset: Union[None, Callable]
startup: Union[None, Callable]
shutdown: Union[None, Callable]
name: str
def __init__(self, router: APIRouter, reset: Callable, startup: Callable, shutdown: Callable, name:str):
self.router = router
self.reset = reset
self.startup = startup
self.shutdown = shutdown
self.name = name
def __repr__(self):
return f"RouterModule(router={self.router}, reset={self.reset}, startup={self.startup}, shutdown={self.shutdown})"
def get_router_modules():
res: List[RouterModule] = []
for route in list_routers():
module = getattr(__import__(f"routers.{route}"), route, None)
if module:
res.append(RouterModule(
router=getattr(module, "app", None),
reset=getattr(module, "reset", None),
startup=getattr(module, "startup", None),
shutdown=getattr(module, "shutdown", None),
name=route
))
return res
def load_routers(app):
resets, startups, shutdowns = [], [], []
for router in get_router_modules():
if router.router:
app.include_router(router.router, prefix=f"/{router.name}")
if router.reset:
resets.append(router.reset)
if router.startup:
startups.append(router.startup)
if router.shutdown:
shutdowns.append(router.shutdown)
async def reset(reset_option:ResetRequest):
for func in resets: await run_func(func, reset_option)
async def startup():
for func in startups: await run_func(func)
async def shutdown():
for func in shutdowns: await run_func(func)
return reset, startup, shutdown

28
backend/utils/models.py Normal file
View File

@@ -0,0 +1,28 @@
from typing import Union
from pydantic import BaseModel
class StatusMessageModel(BaseModel):
status:str
class StatusModel(BaseModel):
status: str
loggined: bool
version: str
class PasswordForm(BaseModel):
password: str
class PasswordChangeForm(BaseModel):
password: str
expire: bool
class ChangePasswordModel(BaseModel):
status: str
access_token: Union[str,None]
class IpInterface(BaseModel):
addr: str
name: str
class ResetRequest(BaseModel):
delete:bool

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/static/css/main.08225a85.css",
"main.js": "/static/js/main.10378d73.js",
"main.js": "/static/js/main.3838510f.js",
"index.html": "/index.html",
"main.08225a85.css.map": "/static/css/main.08225a85.css.map",
"main.10378d73.js.map": "/static/js/main.10378d73.js.map"
"main.3838510f.js.map": "/static/js/main.3838510f.js.map"
},
"entrypoints": [
"static/css/main.08225a85.css",
"static/js/main.10378d73.js"
"static/js/main.3838510f.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"><link rel="manifest" href="/site.webmanifest"><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#FFFFFFFF"/><meta name="description" content="Firegex by Pwnzer0tt1"/><title>Firegex</title><script defer="defer" src="/static/js/main.10378d73.js"></script><link href="/static/css/main.08225a85.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"><link rel="manifest" href="/site.webmanifest"><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#FFFFFFFF"/><meta name="description" content="Firegex by Pwnzer0tt1"/><title>Firegex</title><script defer="defer" src="/static/js/main.3838510f.js"></script><link href="/static/css/main.08225a85.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

View File

@@ -68,15 +68,15 @@ export async function getstatus(){
}
export async function generalstats(){
return await getapi("general-stats") as GeneralStats;
return await getapi("nfregex/stats") as GeneralStats;
}
export async function servicelist(){
return await getapi("services") as Service[];
return await getapi("nfregex/services") as Service[];
}
export async function serviceinfo(service_id:string){
return await getapi(`service/${service_id}`) as Service;
return await getapi(`nfregex/service/${service_id}`) as Service;
}
export async function logout(){
@@ -105,57 +105,55 @@ export async function login(data:PasswordSend) {
}
export async function deleteregex(regex_id:number){
const { status } = await getapi(`regex/${regex_id}/delete`) as ServerResponse;
const { status } = await getapi(`nfregex/regex/${regex_id}/delete`) as ServerResponse;
return status === "ok"?undefined:status
}
export async function activateregex(regex_id:number){
const { status } = await getapi(`regex/${regex_id}/enable`) as ServerResponse;
const { status } = await getapi(`nfregex/regex/${regex_id}/enable`) as ServerResponse;
return status === "ok"?undefined:status
}
export async function deactivateregex(regex_id:number){
const { status } = await getapi(`regex/${regex_id}/disable`) as ServerResponse;
const { status } = await getapi(`nfregex/regex/${regex_id}/disable`) as ServerResponse;
return status === "ok"?undefined:status
}
export async function startservice(service_id:string){
const { status } = await getapi(`service/${service_id}/start`) as ServerResponse;
const { status } = await getapi(`nfregex/service/${service_id}/start`) as ServerResponse;
return status === "ok"?undefined:status
}
export async function renameservice(service_id:string, name: string){
const { status } = await postapi(`service/${service_id}/rename`,{ name }) as ServerResponse;
const { status } = await postapi(`nfregex/service/${service_id}/rename`,{ name }) as ServerResponse;
return status === "ok"?undefined:status
}
export async function stopservice(service_id:string){
const { status } = await getapi(`service/${service_id}/stop`) as ServerResponse;
const { status } = await getapi(`nfregex/service/${service_id}/stop`) as ServerResponse;
return status === "ok"?undefined:status
}
export async function addservice(data:ServiceAddForm) {
return await postapi("services/add",data) as ServiceAddResponse;
return await postapi("nfregex/services/add",data) as ServiceAddResponse;
}
export async function deleteservice(service_id:string) {
const { status } = await getapi(`service/${service_id}/delete`) as ServerResponse;
const { status } = await getapi(`nfregex/service/${service_id}/delete`) as ServerResponse;
return status === "ok"?undefined:status
}
export async function addregex(data:RegexAddForm) {
const { status } = await postapi("regexes/add",data) as ServerResponse;
const { status } = await postapi("nfregex/regexes/add",data) as ServerResponse;
return status === "ok"?undefined:status
}
export async function serviceregexlist(service_id:string){
return await getapi(`service/${service_id}/regexes`) as RegexFilter[];
return await getapi(`nfregex/service/${service_id}/regexes`) as RegexFilter[];
}
export function errorNotify(title:string, description:string ){
showNotification({
autoClose: 2000,