firegex/backend/utils/__init__.py
2026-09-13 15:54:56 +02:00

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