mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 16:31:00 +00:00
308 lines
11 KiB
Python
308 lines
11 KiB
Python
import asyncio
|
|
from ipaddress import ip_address, ip_interface
|
|
import os
|
|
import re
|
|
import socket
|
|
import psutil
|
|
import sys
|
|
try:
|
|
import nftables
|
|
except ImportError:
|
|
nftables = None
|
|
from socketio import AsyncServer
|
|
from typing import Annotated, List, Union
|
|
from pydantic import BaseModel, Field
|
|
import traceback
|
|
from pathlib import Path
|
|
|
|
from fastapi import HTTPException, status
|
|
|
|
_localhost_env = os.getenv("LOCALHOST_IP", "127.0.0.1")
|
|
if _localhost_env == "None":
|
|
_localhost_env = "127.0.0.1"
|
|
LOCALHOST_IP = socket.gethostbyname(_localhost_env)
|
|
|
|
socketio:AsyncServer = None
|
|
sid_list:set = set()
|
|
|
|
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
|
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
|
|
ON_DOCKER = "DOCKER" in sys.argv
|
|
DEBUG = "DEBUG" in sys.argv
|
|
NORELOAD = "NORELOAD" in sys.argv
|
|
_port_env = os.getenv("PORT", "4444")
|
|
FIREGEX_PORT = int(_port_env) if _port_env and _port_env != "None" else 4444
|
|
_host_env = os.getenv("HOST", "0.0.0.0")
|
|
FIREGEX_HOST = _host_env if _host_env and _host_env != "None" else "0.0.0.0"
|
|
_socket_dir_env = os.getenv("SOCKET_DIR", None)
|
|
FIREGEX_SOCKET_DIR = None if _socket_dir_env == "None" else _socket_dir_env
|
|
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
|
|
JWT_ALGORITHM: str = "HS256"
|
|
def _get_version():
|
|
v = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
|
|
if v == "0.0.0":
|
|
env_v = os.getenv("FIREGEX_VERSION")
|
|
if env_v:
|
|
return env_v
|
|
try:
|
|
import re
|
|
|
|
def parse_version(ver):
|
|
ver = ver.lstrip("v")
|
|
parts = re.split(r'[^0-9]+', ver)
|
|
return tuple(int(p) if p.isdigit() else 0 for p in parts if p)
|
|
|
|
def find_git_dir(start_path):
|
|
current = start_path
|
|
while current and current != "/" and current != "":
|
|
gd = os.path.join(current, ".git")
|
|
if os.path.isdir(gd):
|
|
return gd
|
|
parent = os.path.dirname(current)
|
|
if parent == current:
|
|
break
|
|
current = parent
|
|
return None
|
|
|
|
git_dir = find_git_dir(os.path.abspath(os.path.dirname(__file__)))
|
|
if not git_dir:
|
|
return v
|
|
|
|
branch = None
|
|
with open(os.path.join(git_dir, "HEAD"), "r") as f:
|
|
head = f.read().strip()
|
|
if head.startswith("ref: refs/heads/"):
|
|
branch = head.split("ref: refs/heads/")[1]
|
|
|
|
if branch != "main":
|
|
return v
|
|
|
|
tags = []
|
|
try:
|
|
with open(os.path.join(git_dir, "packed-refs"), "r") as f:
|
|
for line in f:
|
|
if " refs/tags/" in line:
|
|
tags.append(line.strip().split(" refs/tags/")[1])
|
|
except Exception:
|
|
pass
|
|
|
|
tags_dir = os.path.join(git_dir, "refs", "tags")
|
|
try:
|
|
for root, _, files in os.walk(tags_dir):
|
|
for file in files:
|
|
tags.append(os.path.relpath(os.path.join(root, file), tags_dir).replace(os.path.sep, "/"))
|
|
except Exception:
|
|
pass
|
|
|
|
version_tags = [t for t in set(tags) if any(c.isdigit() for c in t)]
|
|
if version_tags:
|
|
return max(version_tags, key=parse_version)
|
|
except Exception:
|
|
pass
|
|
return v
|
|
|
|
API_VERSION = _get_version()
|
|
|
|
PortType = Annotated[int, Field(gt=0, lt=65536)]
|
|
|
|
def safe_join(base_dir: Union[str, Path], *paths: str) -> Path:
|
|
"""
|
|
Safely join a base directory with one or more path components.
|
|
|
|
Returns the resolved Path object if safe.
|
|
Raises HTTPException 403 if a directory traversal attack is detected.
|
|
"""
|
|
# 1. Convert base_dir to a resolved, absolute Path
|
|
base_path = Path(base_dir).resolve()
|
|
|
|
# 2. Join the parts and resolve the result (removes symbols like '../')
|
|
# If the user input is an absolute path (starting with /),
|
|
# joinpath handles it correctly or we can strip leading slashes.
|
|
clean_paths = [p.lstrip("/") for p in paths]
|
|
target_path = base_path.joinpath(*clean_paths).resolve()
|
|
|
|
# 3. Security check
|
|
if not target_path.is_relative_to(base_path):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Access denied: Path is invalid."
|
|
)
|
|
|
|
return target_path
|
|
|
|
async def run_func(func, *args, **kwargs):
|
|
if asyncio.iscoroutinefunction(func):
|
|
return await func(*args, **kwargs)
|
|
else:
|
|
return func(*args, **kwargs)
|
|
|
|
async def socketio_emit(elements:list[str]):
|
|
await socketio.emit("update",elements)
|
|
|
|
def refactor_name(name:str):
|
|
name = name.strip()
|
|
while " " in name:
|
|
name = name.replace(" "," ")
|
|
return name
|
|
|
|
class SysctlManager:
|
|
def __init__(self, ctl_table):
|
|
self.old_table = {}
|
|
self.new_table = {}
|
|
if os.path.isdir("/sys_host/"):
|
|
self.old_table = dict()
|
|
self.new_table = dict(ctl_table)
|
|
for name in ctl_table.keys():
|
|
self.old_table[name] = read_sysctl(name)
|
|
|
|
def write_table(self, table) -> bool:
|
|
for name, value in table.items():
|
|
if read_sysctl(name) != value:
|
|
write_sysctl(name, value)
|
|
|
|
def set(self):
|
|
self.write_table(self.new_table)
|
|
|
|
def reset(self):
|
|
self.write_table(self.old_table)
|
|
|
|
def read_sysctl(name:str):
|
|
with open(f"/sys_host/{name}", "rt") as f:
|
|
return "1" in f.read()
|
|
|
|
def write_sysctl(name:str, value:bool):
|
|
with open(f"/sys_host/{name}", "wt") as f:
|
|
f.write("1" if value else "0")
|
|
|
|
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)
|
|
|
|
def is_ip_parse(ip:str):
|
|
try:
|
|
ip_parse(ip)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
def is_interface_name(name: str) -> bool:
|
|
if not isinstance(name, str):
|
|
return False
|
|
name = name.strip()
|
|
return bool(re.match(r'^[a-zA-Z0-9_.:-]+$', name)) and 0 < len(name) <= 15
|
|
|
|
def parse_ip_or_int(ip: str) -> str:
|
|
if is_ip_parse(ip):
|
|
return ip_parse(ip)
|
|
ip_str = str(ip).strip()
|
|
if is_interface_name(ip_str):
|
|
return ip_str
|
|
raise ValueError(f"'{ip}' is neither a valid IP address nor a valid interface name")
|
|
|
|
def addr_parse(ip:str):
|
|
return str(ip_address(ip))
|
|
|
|
def ip_family(ip:str):
|
|
return "ip6" if ip_interface(ip).version == 6 else "ip"
|
|
|
|
def get_interfaces():
|
|
def _get_interfaces():
|
|
for int_name, interfs in psutil.net_if_addrs().items():
|
|
for interf in interfs:
|
|
if interf.family in [socket.AF_INET, socket.AF_INET6]:
|
|
yield {"name": int_name, "addr":interf.address}
|
|
return list(_get_interfaces())
|
|
|
|
def get_interface_ips(iface_name: str) -> list[str]:
|
|
ips = []
|
|
for int_name, interfs in psutil.net_if_addrs().items():
|
|
if int_name == iface_name:
|
|
for interf in interfs:
|
|
if interf.family in [socket.AF_INET, socket.AF_INET6]:
|
|
ips.append(interf.address)
|
|
return ips
|
|
|
|
def nftables_int_to_json(ip_int):
|
|
ip_int = ip_parse(ip_int)
|
|
ip_addr = str(ip_int).split("/")[0]
|
|
ip_addr_cidr = int(str(ip_int).split("/")[1])
|
|
return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}
|
|
|
|
class Singleton(object):
|
|
__instance = None
|
|
def __new__(class_, *args, **kwargs):
|
|
if not isinstance(class_.__instance, class_):
|
|
class_.__instance = object.__new__(class_, *args, **kwargs)
|
|
return class_.__instance
|
|
|
|
class NFTableManager(Singleton):
|
|
|
|
table_name = "fgex"
|
|
|
|
def __init__(self, init_cmd, reset_cmd):
|
|
self.__init_cmds = init_cmd
|
|
self.__reset_cmds = reset_cmd
|
|
self.nft = nftables.Nftables() if nftables is not None else None
|
|
|
|
def raw_cmd(self, *cmds):
|
|
return self.nft.json_cmd({"nftables": list(cmds)})
|
|
|
|
def cmd(self, *cmds):
|
|
code, out, err = self.raw_cmd(*cmds)
|
|
if code == 0:
|
|
return out
|
|
else:
|
|
raise Exception(err)
|
|
|
|
def init(self):
|
|
self.reset()
|
|
self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}})
|
|
self.cmd(*self.__init_cmds)
|
|
|
|
def reset(self):
|
|
self.raw_cmd(*self.__reset_cmds)
|
|
|
|
def list_rules(self, tables = None, chains = None):
|
|
for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]:
|
|
if tables and filter["table"] not in tables:
|
|
continue
|
|
if chains and filter["chain"] not in chains:
|
|
continue
|
|
yield filter
|
|
|
|
def raw_list(self):
|
|
return self.cmd({"list": {"ruleset": None}})["nftables"]
|
|
|
|
def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"):
|
|
res = obj.model_dump(mode=mode, exclude_unset=not unset)
|
|
if convert_keys:
|
|
for from_k, to_k in convert_keys.items():
|
|
if from_k in res:
|
|
res[to_k] = res.pop(from_k)
|
|
if exclude:
|
|
for ele in exclude:
|
|
if ele in res:
|
|
del res[ele]
|
|
return res
|
|
|
|
def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict:
|
|
if isinstance(obj, list):
|
|
return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj]
|
|
return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode)
|
|
|
|
def nicenessify(priority:int, pid:int|None=None):
|
|
try:
|
|
pid = os.getpid() if pid is None else pid
|
|
ps = psutil.Process(pid)
|
|
if os.name == 'posix':
|
|
ps.nice(priority)
|
|
except (psutil.AccessDenied, PermissionError) as e:
|
|
print(f"Permission denied setting priority (pid={pid}, priority={priority}): {e}")
|
|
except Exception as e:
|
|
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
|
pass
|