2022-07-21 01:02:46 +02:00
from base64 import b64decode
import re
2022-07-21 20:25:39 +02:00
import secrets
2022-07-21 01:02:46 +02:00
import sqlite3
from fastapi import APIRouter , HTTPException
from pydantic import BaseModel
2022-08-10 10:23:37 +00:00
from modules . nfregex . nftables import FiregexTables
2022-07-21 20:25:39 +02:00
from modules . nfregex . firewall import STATUS , FirewallManager
from utils . sqlite import SQLite
2023-09-22 20:46:50 +02:00
from utils import ip_parse , refactor_name , refresh_frontend , PortType
2022-07-21 01:02:46 +02:00
from utils . models import ResetRequest , StatusMessageModel
class ServiceModel ( BaseModel ) :
status : str
service_id : str
2023-09-22 20:46:50 +02:00
port : PortType
2022-07-21 01:02:46 +02:00
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
2023-09-23 02:02:02 +02:00
active : bool | None = None
2022-07-21 01:02:46 +02:00
is_blacklist : bool
is_case_sensitive : bool
class ServiceAddForm ( BaseModel ) :
name : str
2023-09-22 20:46:50 +02:00
port : PortType
2022-07-21 01:02:46 +02:00
proto : str
ip_int : str
class ServiceAddResponse ( BaseModel ) :
status : str
2023-09-23 02:02:02 +02:00
service_id : str | None = None
2022-07-21 01:02:46 +02:00
app = APIRouter ( )
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 ' ,
2023-09-22 20:46:50 +02:00
' mode ' : ' VARCHAR(1) NOT NULL CHECK (mode IN ( " C " , " S " , " B " )) ' , # C = to the client, S = to the server, B = both
2022-07-21 01:02:46 +02:00
' 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); "
]
} )
2022-07-21 20:25:39 +02:00
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 ( )
def gen_service_id ( ) :
while True :
res = secrets . token_hex ( 8 )
if len ( db . query ( ' SELECT 1 FROM services WHERE service_id = ?; ' , res ) ) == 0 :
break
return res
2022-07-21 01:02:46 +02:00
firewall = FirewallManager ( db )
2023-09-22 20:46:50 +02:00
@app.get ( ' /services ' , response_model = list [ ServiceModel ] )
2022-07-21 01:02:46 +02:00
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 )
2022-08-11 18:01:07 +00:00
async def get_service_by_id ( service_id : str ) :
2022-07-21 01:02:46 +02:00
""" 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 )
2022-08-11 18:01:07 +00:00
async def service_stop ( service_id : str ) :
2022-07-21 01:02:46 +02:00
""" 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 )
2022-08-11 18:01:07 +00:00
async def service_start ( service_id : str ) :
2022-07-21 01:02:46 +02:00
""" 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 )
2022-08-11 18:01:07 +00:00
async def service_delete ( service_id : str ) :
2022-07-21 01:02:46 +02:00
""" 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 )
2022-08-11 18:01:07 +00:00
async def service_rename ( service_id : str , form : RenameForm ) :
2022-07-21 01:02:46 +02:00
""" Request to change the name of a specific service """
form . name = refactor_name ( form . name )
2023-09-25 18:10:12 +02:00
if not form . name : raise HTTPException ( status_code = 400 , detail = " The name cannot be empty! " )
2022-07-21 01:02:46 +02:00
try :
db . query ( ' UPDATE services SET name=? WHERE service_id = ?; ' , form . name , service_id )
except sqlite3 . IntegrityError :
2023-09-25 18:10:12 +02:00
raise HTTPException ( status_code = 400 , detail = " This name is already used " )
2022-07-21 01:02:46 +02:00
await refresh_frontend ( )
return { ' status ' : ' ok ' }
2023-09-22 20:46:50 +02:00
@app.get ( ' /service/ {service_id} /regexes ' , response_model = list [ RegexModel ] )
2022-08-11 18:01:07 +00:00
async def get_service_regexe_list ( service_id : str ) :
2022-07-21 01:02:46 +02:00
""" Get the list of the regexes of a service """
2023-09-24 05:48:54 +02:00
if not db . query ( " SELECT 1 FROM services s WHERE s.service_id = ?; " , service_id ) : raise HTTPException ( status_code = 400 , detail = " This service does not exists! " )
2022-07-21 01:02:46 +02:00
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 )
2022-08-11 18:01:07 +00:00
async def get_regex_by_id ( regex_id : int ) :
2022-07-21 01:02:46 +02:00
""" 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 )
2022-08-11 18:01:07 +00:00
async def regex_delete ( regex_id : int ) :
2022-07-21 01:02:46 +02:00
""" 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 )
2022-08-11 18:01:07 +00:00
async def regex_enable ( regex_id : int ) :
2022-07-21 01:02:46 +02:00
""" 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 )
2022-08-11 18:01:07 +00:00
async def regex_disable ( regex_id : int ) :
2022-07-21 01:02:46 +02:00
""" 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 )
2022-08-11 18:01:07 +00:00
async def add_new_regex ( form : RegexAddForm ) :
2022-07-21 01:02:46 +02:00
""" Add a new regex """
try :
re . compile ( b64decode ( form . regex ) )
except Exception :
2023-09-25 18:10:12 +02:00
raise HTTPException ( status_code = 400 , detail = " Invalid regex " )
2022-07-21 01:02:46 +02:00
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 :
2023-09-25 18:10:12 +02:00
raise HTTPException ( status_code = 400 , detail = " An identical regex already exists " )
2022-07-21 01:02:46 +02:00
await firewall . get ( form . service_id ) . update_filters ( )
await refresh_frontend ( )
return { ' status ' : ' ok ' }
@app.post ( ' /services/add ' , response_model = ServiceAddResponse )
2022-08-11 18:01:07 +00:00
async def add_new_service ( form : ServiceAddForm ) :
2022-07-21 01:02:46 +02:00
""" Add a new service """
try :
form . ip_int = ip_parse ( form . ip_int )
except ValueError :
2023-09-25 18:10:12 +02:00
raise HTTPException ( status_code = 400 , detail = " Invalid address " )
2022-07-21 01:02:46 +02:00
if form . proto not in [ " tcp " , " udp " ] :
2023-09-25 18:10:12 +02:00
raise HTTPException ( status_code = 400 , detail = " Invalid protocol " )
2022-07-21 01:02:46 +02:00
srv_id = None
try :
2022-07-21 20:25:39 +02:00
srv_id = gen_service_id ( )
2022-07-21 01:02:46 +02:00
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 :
2023-09-25 18:10:12 +02:00
raise HTTPException ( status_code = 400 , detail = " This type of service already exists " )
2022-07-21 01:02:46 +02:00
await firewall . reload ( )
await refresh_frontend ( )
return { ' status ' : ' ok ' , ' service_id ' : srv_id }