From f3ba6dc716513b0ba521d594ff32ff3e399474b2 Mon Sep 17 00:00:00 2001 From: Domingo Dirutigliano Date: Tue, 11 Feb 2025 19:11:30 +0100 Subject: [PATCH] more RESTful APIs --- Dockerfile | 11 +- backend/binsrc/classes/netfilter.cpp | 6 - backend/binsrc/proxytun/proxytun.cpp | 4 - backend/modules/firewall/firewall.py | 5 +- backend/modules/nfproxy/firegex.py | 2 + backend/modules/nfproxy/firewall.py | 2 + backend/modules/nfproxy/models.py | 19 +- backend/routers/firewall.py | 10 +- backend/routers/nfproxy.py | 260 ++++++++++++++++++ backend/routers/nfregex.py | 27 +- backend/routers/porthijack.py | 14 +- frontend/src/components/Firewall/utils.ts | 10 +- frontend/src/components/NFRegex/utils.ts | 24 +- .../PortHijack/ServiceRow/index.tsx | 34 +-- frontend/src/components/PortHijack/utils.ts | 16 +- frontend/src/js/utils.tsx | 45 ++- start.py | 1 + tests/utils/firegexapi.py | 51 ++-- 18 files changed, 378 insertions(+), 163 deletions(-) create mode 100644 backend/routers/nfproxy.py diff --git a/Dockerfile b/Dockerfile index 58a7283..7eea0ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,11 +14,10 @@ RUN bun run build #Building main conteiner -FROM --platform=$TARGETARCH debian:trixie-slim AS base -RUN apt-get update -qq && apt-get upgrade -qq && \ - apt-get install -qq python3-pip build-essential \ - libnetfilter-queue-dev libnfnetlink-dev libmnl-dev libcap2-bin\ - nftables libvectorscan-dev libtins-dev python3-nftables +FROM --platform=$TARGETARCH registry.fedoraproject.org/fedora:latest +RUN dnf -y update && dnf install -y python3-pip @development-tools gcc-c++ \ + libnetfilter_queue-devel libnfnetlink-devel libmnl-devel libcap-ng-utils \ + nftables vectorscan-devel libtins-devel python3-nftables libpcap-devel boost-devel RUN mkdir -p /execute/modules WORKDIR /execute @@ -28,7 +27,7 @@ RUN pip3 install --no-cache-dir --break-system-packages -r /execute/requirements COPY ./backend/binsrc /execute/binsrc RUN g++ binsrc/nfqueue.cpp -o modules/cppqueue -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl) -RUN g++ binsrc/nfproxy-tun.cpp -o modules/cppnfproxy -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl) +#RUN g++ binsrc/nfproxy-tun.cpp -o modules/cppnfproxy -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl) COPY ./backend/ /execute/ COPY --from=frontend /app/dist/ ./frontend/ diff --git a/backend/binsrc/classes/netfilter.cpp b/backend/binsrc/classes/netfilter.cpp index fed457c..5ac7c1b 100644 --- a/backend/binsrc/classes/netfilter.cpp +++ b/backend/binsrc/classes/netfilter.cpp @@ -1,20 +1,14 @@ #include #include #include -#include -#include -#include #include #include #include #include #include #include -#include #include -using Tins::TCPIP::Stream; -using Tins::TCPIP::StreamFollower; using namespace std; #ifndef NETFILTER_CLASS_CPP diff --git a/backend/binsrc/proxytun/proxytun.cpp b/backend/binsrc/proxytun/proxytun.cpp index 22c88ac..dbce409 100644 --- a/backend/binsrc/proxytun/proxytun.cpp +++ b/backend/binsrc/proxytun/proxytun.cpp @@ -133,10 +133,6 @@ class SocketTunnelQueue: public NfQueueExecutor { SocketTunnelQueue(int queue) : NfQueueExecutor(queue, &queue_cb) {} - ~SocketTunnelQueue() { - // TODO - } - }; #endif // PROXY_TUNNEL_CPP \ No newline at end of file diff --git a/backend/modules/firewall/firewall.py b/backend/modules/firewall/firewall.py index b5bb292..171e34f 100644 --- a/backend/modules/firewall/firewall.py +++ b/backend/modules/firewall/firewall.py @@ -130,6 +130,7 @@ class FirewallManager: def allow_dhcp(self): return self.db.get("allow_dhcp", "1") == "1" - @drop_invalid.setter - def allow_dhcp_set(self, value): + @allow_dhcp.setter + def allow_dhcp(self, value): self.db.set("allow_dhcp", "1" if value else "0") + diff --git a/backend/modules/nfproxy/firegex.py b/backend/modules/nfproxy/firegex.py index 71afcf8..6f0e8d2 100644 --- a/backend/modules/nfproxy/firegex.py +++ b/backend/modules/nfproxy/firegex.py @@ -8,6 +8,8 @@ import traceback from utils import DEBUG from fastapi import HTTPException +#TODO copied file, review + nft = FiregexTables() class RegexFilter: diff --git a/backend/modules/nfproxy/firewall.py b/backend/modules/nfproxy/firewall.py index d0d5479..5ff1c39 100644 --- a/backend/modules/nfproxy/firewall.py +++ b/backend/modules/nfproxy/firewall.py @@ -4,6 +4,8 @@ from modules.nfregex.nftables import FiregexTables, FiregexFilter from modules.nfregex.models import Regex, Service from utils.sqlite import SQLite +#TODO copied file, review + class STATUS: STOP = "stop" ACTIVE = "active" diff --git a/backend/modules/nfproxy/models.py b/backend/modules/nfproxy/models.py index 0c36890..24d1087 100644 --- a/backend/modules/nfproxy/models.py +++ b/backend/modules/nfproxy/models.py @@ -1,5 +1,3 @@ -import base64 - class Service: def __init__(self, service_id: str, status: str, port: int, name: str, proto: str, ip_int: str, **other): self.id = service_id @@ -14,17 +12,14 @@ class Service: return cls(**var) -class Regex: - def __init__(self, regex_id: int, regex: bytes, mode: str, service_id: str, blocked_packets: int, is_case_sensitive: bool, active: bool, **other): - self.regex = regex - self.mode = mode - self.service_id = service_id +class PyFilter: + def __init__(self, filter_id:int, name: str, blocked_packets: int, edited_packets: int, active: bool, **other): + self.filter_id = filter_id + self.name = name self.blocked_packets = blocked_packets - self.id = regex_id - self.is_case_sensitive = is_case_sensitive + self.edited_packets = edited_packets self.active = active - + @classmethod def from_dict(cls, var: dict): - var['regex'] = base64.b64decode(var['regex']) - return cls(**var) \ No newline at end of file + return cls(**var) diff --git a/backend/routers/firewall.py b/backend/routers/firewall.py index 8801db9..058d6a1 100644 --- a/backend/routers/firewall.py +++ b/backend/routers/firewall.py @@ -71,7 +71,7 @@ async def get_settings(): """Get the firewall settings""" return firewall.settings -@app.post("/settings/set", response_model=StatusMessageModel) +@app.put("/settings", response_model=StatusMessageModel) async def set_settings(form: FirewallSettings): """Set the firewall settings""" firewall.settings = form @@ -86,13 +86,13 @@ async def get_rule_list(): "enabled": firewall.enabled } -@app.get('/enable', response_model=StatusMessageModel) +@app.post('/enable', response_model=StatusMessageModel) async def enable_firewall(): """Request enabling the firewall""" firewall.enabled = True return await apply_changes() -@app.get('/disable', response_model=StatusMessageModel) +@app.post('/disable', response_model=StatusMessageModel) async def disable_firewall(): """Request disabling the firewall""" firewall.enabled = False @@ -128,9 +128,9 @@ def parse_and_check_rule(rule:RuleModel): return rule -@app.post('/rules/set', response_model=StatusMessageModel) +@app.post('/rules', response_model=StatusMessageModel) async def add_new_service(form: RuleFormAdd): - """Add a new service""" + """Edit rule table""" rules = [parse_and_check_rule(ele) for ele in form.rules] try: db.queries(["DELETE FROM rules"]+ diff --git a/backend/routers/nfproxy.py b/backend/routers/nfproxy.py new file mode 100644 index 0000000..8580404 --- /dev/null +++ b/backend/routers/nfproxy.py @@ -0,0 +1,260 @@ +import secrets +import sqlite3 +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from modules.nfproxy.nftables import FiregexTables +from modules.nfproxy.firewall import STATUS, FirewallManager +from utils.sqlite import SQLite +from utils import ip_parse, refactor_name, socketio_emit, PortType +from utils.models import ResetRequest, StatusMessageModel + +# TODO copied file, review +class ServiceModel(BaseModel): + service_id: str + status: str + port: PortType + name: str + proto: str + ip_int: str + n_filters: int + edited_packets: int + blocked_packets: int + +class RenameForm(BaseModel): + name:str + +class PyFilterModel(BaseModel): + filter_id: int + name: str + blocked_packets: int + edited_packets: int + active: bool + +class ServiceAddForm(BaseModel): + name: str + port: PortType + proto: str + ip_int: str + +class ServiceAddResponse(BaseModel): + status:str + service_id: str|None = None + +app = APIRouter() + +db = SQLite('db/nft-pyfilters.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", "http"))', + 'ip_int': 'VARCHAR(100) NOT NULL', + }, + 'pyfilter': { + 'filter_id': 'INTEGER PRIMARY KEY', + 'name': 'VARCHAR(100) NOT NULL', + 'blocked_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0', + 'edited_packets': 'INTEGER UNSIGNED NOT NULL DEFAULT 0', + 'service_id': 'VARCHAR(100) NOT NULL', + '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_pyfilter_service ON pyfilter (name, service_id);" + ] +}) + +async def refresh_frontend(additional:list[str]=[]): + await socketio_emit(["nfproxy"]+additional) + +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() + try: + await firewall.init() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +async def startup(): + db.init() + try: + await firewall.init() + except Exception as e: + print("WARNING cannot start firewall:", e) + +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 + +firewall = FirewallManager(db) + +@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(f.filter_id) n_filters, + COALESCE(SUM(f.blocked_packets),0) blocked_packets, + COALESCE(SUM(f.edited_packets),0) edited_packets + FROM services s LEFT JOIN pyfilter f ON s.service_id = f.service_id + GROUP BY s.service_id; + """) + +@app.get('/services/{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(f.filter_id) n_filters, + COALESCE(SUM(f.blocked_packets),0) blocked_packets, + COALESCE(SUM(f.edited_packets),0) edited_packets + FROM services s LEFT JOIN pyfilter f ON s.service_id = f.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.post('/services/{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.post('/services/{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.delete('/services/{service_id}', 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 pyfilter WHERE service_id = ?;', service_id) + await firewall.remove(service_id) + await refresh_frontend() + return {'status': 'ok'} + +@app.put('/services/{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: + raise HTTPException(status_code=400, detail="The name cannot be empty!") + try: + db.query('UPDATE services SET name=? WHERE service_id = ?;', form.name, service_id) + except sqlite3.IntegrityError: + raise HTTPException(status_code=400, detail="This name is already used") + await refresh_frontend() + return {'status': 'ok'} + +@app.get('/services/{service_id}/pyfilters', response_model=list[PyFilterModel]) +async def get_service_pyfilter_list(service_id: str): + """Get the list of the pyfilters of a service""" + 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!") + return db.query(""" + SELECT + filter_id, name, blocked_packets, edited_packets, active + FROM pyfilter WHERE service_id = ?; + """, service_id) + +@app.get('/pyfilters/{filter_id}', response_model=PyFilterModel) +async def get_pyfilter_by_id(filter_id: int): + """Get pyfilter info using his id""" + res = db.query(""" + SELECT + filter_id, name, blocked_packets, edited_packets, active + FROM pyfilter WHERE filter_id = ?; + """, filter_id) + if len(res) == 0: + raise HTTPException(status_code=400, detail="This filter does not exists!") + return res[0] + +@app.delete('/pyfilters/{filter_id}', response_model=StatusMessageModel) +async def pyfilter_delete(filter_id: int): + """Delete a pyfilter using his id""" + res = db.query('SELECT * FROM pyfilter WHERE filter_id = ?;', filter_id) + if len(res) != 0: + db.query('DELETE FROM pyfilter WHERE filter_id = ?;', filter_id) + await firewall.get(res[0]["service_id"]).update_filters() + await refresh_frontend() + + return {'status': 'ok'} + +@app.post('/pyfilters/{filter_id}/enable', response_model=StatusMessageModel) +async def pyfilter_enable(filter_id: int): + """Request the enabling of a pyfilter""" + res = db.query('SELECT * FROM pyfilter WHERE filter_id = ?;', filter_id) + if len(res) != 0: + db.query('UPDATE pyfilter SET active=1 WHERE filter_id = ?;', filter_id) + await firewall.get(res[0]["service_id"]).update_filters() + await refresh_frontend() + return {'status': 'ok'} + +@app.post('/pyfilters/{filter_id}/disable', response_model=StatusMessageModel) +async def pyfilter_disable(filter_id: int): + """Request the deactivation of a pyfilter""" + res = db.query('SELECT * FROM pyfilter WHERE filter_id = ?;', filter_id) + if len(res) != 0: + db.query('UPDATE pyfilter SET active=0 WHERE filter_id = ?;', filter_id) + await firewall.get(res[0]["service_id"]).update_filters() + await refresh_frontend() + return {'status': 'ok'} + +@app.post('/services', 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: + raise HTTPException(status_code=400, detail="Invalid address") + if form.proto not in ["tcp", "http"]: + raise HTTPException(status_code=400, detail="Invalid protocol") + srv_id = None + try: + srv_id = gen_service_id() + 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: + raise HTTPException(status_code=400, detail="This type of service already exists") + await firewall.reload() + await refresh_frontend() + return {'status': 'ok', 'service_id': srv_id} + +#TODO check all the APIs and add +# 1. API to change the python filter file +# 2. a socketio mechanism to lock the previous feature \ No newline at end of file diff --git a/backend/routers/nfregex.py b/backend/routers/nfregex.py index f5de063..1a41d41 100644 --- a/backend/routers/nfregex.py +++ b/backend/routers/nfregex.py @@ -134,7 +134,7 @@ async def get_service_list(): GROUP BY s.service_id; """) -@app.get('/service/{service_id}', response_model=ServiceModel) +@app.get('/services/{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(""" @@ -154,21 +154,21 @@ async def get_service_by_id(service_id: str): raise HTTPException(status_code=400, detail="This service does not exists!") return res[0] -@app.get('/service/{service_id}/stop', response_model=StatusMessageModel) +@app.post('/services/{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) +@app.post('/services/{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) +@app.delete('/services/{service_id}', 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) @@ -177,7 +177,7 @@ async def service_delete(service_id: str): await refresh_frontend() return {'status': 'ok'} -@app.post('/service/{service_id}/rename', response_model=StatusMessageModel) +@app.put('/services/{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) @@ -190,7 +190,7 @@ async def service_rename(service_id: str, form: RenameForm): await refresh_frontend() return {'status': 'ok'} -@app.get('/service/{service_id}/regexes', response_model=list[RegexModel]) +@app.get('/services/{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""" if not db.query("SELECT 1 FROM services s WHERE s.service_id = ?;", service_id): @@ -202,7 +202,7 @@ async def get_service_regexe_list(service_id: str): FROM regexes WHERE service_id = ?; """, service_id) -@app.get('/regex/{regex_id}', response_model=RegexModel) +@app.get('/regexes/{regex_id}', response_model=RegexModel) async def get_regex_by_id(regex_id: int): """Get regex info using his id""" res = db.query(""" @@ -215,7 +215,7 @@ async def get_regex_by_id(regex_id: int): raise HTTPException(status_code=400, detail="This regex does not exists!") return res[0] -@app.get('/regex/{regex_id}/delete', response_model=StatusMessageModel) +@app.delete('/regexes/{regex_id}', 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) @@ -226,7 +226,7 @@ async def regex_delete(regex_id: int): return {'status': 'ok'} -@app.get('/regex/{regex_id}/enable', response_model=StatusMessageModel) +@app.post('/regexes/{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) @@ -236,7 +236,7 @@ async def regex_enable(regex_id: int): await refresh_frontend() return {'status': 'ok'} -@app.get('/regex/{regex_id}/disable', response_model=StatusMessageModel) +@app.post('/regexes/{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) @@ -246,7 +246,7 @@ async def regex_disable(regex_id: int): await refresh_frontend() return {'status': 'ok'} -@app.post('/regexes/add', response_model=StatusMessageModel) +@app.post('/regexes', response_model=StatusMessageModel) async def add_new_regex(form: RegexAddForm): """Add a new regex""" try: @@ -263,7 +263,7 @@ async def add_new_regex(form: RegexAddForm): await refresh_frontend() return {'status': 'ok'} -@app.post('/services/add', response_model=ServiceAddResponse) +@app.post('/services', response_model=ServiceAddResponse) async def add_new_service(form: ServiceAddForm): """Add a new service""" try: @@ -299,7 +299,8 @@ async def metrics(): FROM regexes r LEFT JOIN services s ON s.service_id = r.service_id; """) metrics = [] - sanitize = lambda s : s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') + def sanitize(s): + return s.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') for stat in stats: props = f'service_name="{sanitize(stat["name"])}",regex="{sanitize(b64decode(stat["regex"]).decode())}",mode="{stat["mode"]}",is_case_sensitive="{stat["is_case_sensitive"]}"' metrics.append(f'firegex_blocked_packets{{{props}}} {stat["blocked_packets"]}') diff --git a/backend/routers/porthijack.py b/backend/routers/porthijack.py index 8fd3c54..7899ef4 100644 --- a/backend/routers/porthijack.py +++ b/backend/routers/porthijack.py @@ -92,7 +92,7 @@ async def get_service_list(): """Get the list of existent firegex services""" return db.query("SELECT service_id, active, public_port, proxy_port, name, proto, ip_src, ip_dst FROM services;") -@app.get('/service/{service_id}', response_model=ServiceModel) +@app.get('/services/{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 service_id, active, public_port, proxy_port, name, proto, ip_src, ip_dst FROM services WHERE service_id = ?;", service_id) @@ -100,21 +100,21 @@ async def get_service_by_id(service_id: str): raise HTTPException(status_code=400, detail="This service does not exists!") return res[0] -@app.get('/service/{service_id}/stop', response_model=StatusMessageModel) +@app.post('/services/{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).disable() await refresh_frontend() return {'status': 'ok'} -@app.get('/service/{service_id}/start', response_model=StatusMessageModel) +@app.post('/services/{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).enable() await refresh_frontend() return {'status': 'ok'} -@app.get('/service/{service_id}/delete', response_model=StatusMessageModel) +@app.delete('/services/{service_id}', 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) @@ -122,7 +122,7 @@ async def service_delete(service_id: str): await refresh_frontend() return {'status': 'ok'} -@app.post('/service/{service_id}/rename', response_model=StatusMessageModel) +@app.put('/services/{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) @@ -139,7 +139,7 @@ class ChangeDestination(BaseModel): ip_dst: str proxy_port: PortType -@app.post('/service/{service_id}/change-destination', response_model=StatusMessageModel) +@app.put('/services/{service_id}/change-destination', response_model=StatusMessageModel) async def service_change_destination(service_id: str, form: ChangeDestination): """Request to change the proxy destination of the service""" @@ -162,7 +162,7 @@ async def service_change_destination(service_id: str, form: ChangeDestination): await refresh_frontend() return {'status': 'ok'} -@app.post('/services/add', response_model=ServiceAddResponse) +@app.post('/services', response_model=ServiceAddResponse) async def add_new_service(form: ServiceAddForm): """Add a new service""" try: diff --git a/frontend/src/components/Firewall/utils.ts b/frontend/src/components/Firewall/utils.ts index fa2419b..c051df8 100644 --- a/frontend/src/components/Firewall/utils.ts +++ b/frontend/src/components/Firewall/utils.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query" import { ServerResponse } from "../../js/models" -import { getapi, postapi } from "../../js/utils" +import { getapi, postapi, putapi } from "../../js/utils" export enum Protocol { TCP = "tcp", @@ -79,15 +79,15 @@ export const firewall = { return await getapi("firewall/settings") as FirewallSettings; }, setsettings: async(data:FirewallSettings) => { - return await postapi("firewall/settings/set", data) as ServerResponse; + return await putapi("firewall/settings", data) as ServerResponse; }, enable: async() => { - return await getapi("firewall/enable") as ServerResponse; + return await postapi("firewall/enable") as ServerResponse; }, disable: async() => { - return await getapi("firewall/disable") as ServerResponse; + return await postapi("firewall/disable") as ServerResponse; }, ruleset: async (data:RuleAddForm) => { - return await postapi("firewall/rules/set", data) as ServerResponseListed; + return await postapi("firewall/rules", data) as ServerResponseListed; } } \ No newline at end of file diff --git a/frontend/src/components/NFRegex/utils.ts b/frontend/src/components/NFRegex/utils.ts index cdf35f8..1c34bc3 100644 --- a/frontend/src/components/NFRegex/utils.ts +++ b/frontend/src/components/NFRegex/utils.ts @@ -1,5 +1,5 @@ import { RegexFilter, ServerResponse } from "../../js/models" -import { getapi, postapi } from "../../js/utils" +import { deleteapi, getapi, postapi, putapi } from "../../js/utils" import { RegexAddForm } from "../../js/models" import { useQuery, useQueryClient } from "@tanstack/react-query" @@ -40,44 +40,44 @@ export const nfregex = { return await getapi("nfregex/services") as Service[]; }, serviceinfo: async (service_id:string) => { - return await getapi(`nfregex/service/${service_id}`) as Service; + return await getapi(`nfregex/services/${service_id}`) as Service; }, regexdelete: async (regex_id:number) => { - const { status } = await getapi(`nfregex/regex/${regex_id}/delete`) as ServerResponse; + const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse; return status === "ok"?undefined:status }, regexenable: async (regex_id:number) => { - const { status } = await getapi(`nfregex/regex/${regex_id}/enable`) as ServerResponse; + const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse; return status === "ok"?undefined:status }, regexdisable: async (regex_id:number) => { - const { status } = await getapi(`nfregex/regex/${regex_id}/disable`) as ServerResponse; + const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse; return status === "ok"?undefined:status }, servicestart: async (service_id:string) => { - const { status } = await getapi(`nfregex/service/${service_id}/start`) as ServerResponse; + const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse; return status === "ok"?undefined:status }, servicerename: async (service_id:string, name: string) => { - const { status } = await postapi(`nfregex/service/${service_id}/rename`,{ name }) as ServerResponse; + const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse; return status === "ok"?undefined:status }, servicestop: async (service_id:string) => { - const { status } = await getapi(`nfregex/service/${service_id}/stop`) as ServerResponse; + const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse; return status === "ok"?undefined:status }, servicesadd: async (data:ServiceAddForm) => { - return await postapi("nfregex/services/add",data) as ServiceAddResponse; + return await postapi("nfregex/services",data) as ServiceAddResponse; }, servicedelete: async (service_id:string) => { - const { status } = await getapi(`nfregex/service/${service_id}/delete`) as ServerResponse; + const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse; return status === "ok"?undefined:status }, regexesadd: async (data:RegexAddForm) => { - const { status } = await postapi("nfregex/regexes/add",data) as ServerResponse; + const { status } = await postapi("nfregex/regexes",data) as ServerResponse; return status === "ok"?undefined:status }, serviceregexes: async (service_id:string) => { - return await getapi(`nfregex/service/${service_id}/regexes`) as RegexFilter[]; + return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[]; } } \ No newline at end of file diff --git a/frontend/src/components/PortHijack/ServiceRow/index.tsx b/frontend/src/components/PortHijack/ServiceRow/index.tsx index 9428170..d481f75 100644 --- a/frontend/src/components/PortHijack/ServiceRow/index.tsx +++ b/frontend/src/components/PortHijack/ServiceRow/index.tsx @@ -29,24 +29,6 @@ function ServiceRow({ service }:{ service:Service }) { validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" } }) - const onChangeProxyPort = ({proxy_port}:{proxy_port:number}) => { - if (proxy_port === service.proxy_port) return - if (proxy_port > 0 && proxy_port < 65536 && proxy_port !== service.public_port){ - porthijack.changedestination(service.service_id, service.ip_dst, proxy_port).then( res => { - if (res.status === "ok"){ - okNotify(`Service ${service.name} destination port has changed in ${ proxy_port }`, `Successfully changed destination port`) - }else{ - errorNotify(`Error while changing the destination port of ${service.name}`,`Error: ${res.status}`) - } - }).catch( err => { - errorNotify("Request for changing port failed!",`Error: [ ${err} ]`) - }) - }else{ - form.setFieldValue("proxy_port", service.proxy_port) - errorNotify(`Error while changing the destination port of ${service.name}`,`Insert a valid port number`) - } - } - const stopService = async () => { setButtonLoading(true) @@ -119,21 +101,7 @@ function ServiceRow({ service }:{ service:Service }) { - TO {service.ip_dst} : -
portInputRef.current?.blur())}> - {onChangeProxyPort({proxy_port:parseInt(e.target.value)})}} - ref={portInputRef} - {...form.getInputProps("proxy_port")} - /> - + TO {service.ip_dst} : service.proxy_port
diff --git a/frontend/src/components/PortHijack/utils.ts b/frontend/src/components/PortHijack/utils.ts index 80875cc..2c00417 100644 --- a/frontend/src/components/PortHijack/utils.ts +++ b/frontend/src/components/PortHijack/utils.ts @@ -1,5 +1,5 @@ import { ServerResponse } from "../../js/models" -import { getapi, postapi } from "../../js/utils" +import { deleteapi, getapi, postapi, putapi } from "../../js/utils" import { useQuery } from "@tanstack/react-query" export type GeneralStats = { @@ -37,28 +37,28 @@ export const porthijack = { return await getapi("porthijack/services") as Service[]; }, serviceinfo: async (service_id:string) => { - return await getapi(`porthijack/service/${service_id}`) as Service; + return await getapi(`porthijack/services/${service_id}`) as Service; }, servicestart: async (service_id:string) => { - const { status } = await getapi(`porthijack/service/${service_id}/start`) as ServerResponse; + const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse; return status === "ok"?undefined:status }, servicerename: async (service_id:string, name: string) => { - const { status } = await postapi(`porthijack/service/${service_id}/rename`,{ name }) as ServerResponse; + const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse; return status === "ok"?undefined:status }, servicestop: async (service_id:string) => { - const { status } = await getapi(`porthijack/service/${service_id}/stop`) as ServerResponse; + const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse; return status === "ok"?undefined:status }, servicesadd: async (data:ServiceAddForm) => { - return await postapi("porthijack/services/add",data) as ServiceAddResponse; + return await postapi("porthijack/services",data) as ServiceAddResponse; }, servicedelete: async (service_id:string) => { - const { status } = await getapi(`porthijack/service/${service_id}/delete`) as ServerResponse; + const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse; return status === "ok"?undefined:status }, changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => { - return await postapi(`porthijack/service/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse; + return await putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse; } } \ No newline at end of file diff --git a/frontend/src/js/utils.tsx b/frontend/src/js/utils.tsx index ba84bcf..0197205 100644 --- a/frontend/src/js/utils.tsx +++ b/frontend/src/js/utils.tsx @@ -22,26 +22,6 @@ export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: Infinity } }}) -export async function getapi(path:string):Promise{ - - return await new Promise((resolve, reject) => { - fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`,{ - credentials: "same-origin", - headers: { "Authorization" : "Bearer " + window.localStorage.getItem("access_token")} - }).then(res => { - if(res.status === 401) window.location.reload() - if(!res.ok){ - const errorDefault = res.statusText - return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault)) - } - res.json().then( res => resolve(res) ).catch( err => reject(err)) - }) - .catch(err => { - reject(err) - }) - }); -} - export function getErrorMessage(e: any) { let error = "Unknown error"; if(typeof e == "string") return e @@ -56,7 +36,6 @@ export function getErrorMessage(e: any) { return error; } - export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") { if (e.status){ return e.status @@ -74,17 +53,17 @@ export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown } -export async function postapi(path:string,data:any,is_form:boolean=false):Promise{ +export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise{ return await new Promise((resolve, reject) => { fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, { - method: 'POST', + method: method, credentials: "same-origin", cache: 'no-cache', headers: { - 'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json', + ...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}), "Authorization" : "Bearer " + window.localStorage.getItem("access_token") }, - body: is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data) + body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined }).then(res => { if(res.status === 401) window.location.reload() if(res.status === 406) resolve({status:"Wrong Password"}) @@ -100,6 +79,22 @@ export async function postapi(path:string,data:any,is_form:boolean=false):Promis }); } +export async function getapi(path:string):Promise{ + return await genericapi("GET",path) +} + +export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise{ + return await genericapi("POST",path,data,is_form) +} + +export async function deleteapi(path:string):Promise{ + return await genericapi("DELETE",path) +} + +export async function putapi(path:string,data:any):Promise{ + return await genericapi("PUT",path,data) +} + export function getMainPath(){ const paths = window.location.pathname.split("/") if (paths.length > 1) return paths[1] diff --git a/start.py b/start.py index 7ceb55a..ca7d1ff 100755 --- a/start.py +++ b/start.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 + from __future__ import annotations import argparse import sys diff --git a/tests/utils/firegexapi.py b/tests/utils/firegexapi.py index ce4a92c..13f7a0c 100644 --- a/tests/utils/firegexapi.py +++ b/tests/utils/firegexapi.py @@ -19,11 +19,17 @@ class BearerSession(): headers["Content-Type"] = "application/x-www-form-urlencoded" return self.s.post(endpoint, json=json, data=data, headers=headers) + def delete(self, endpoint, json={}): + return self.s.delete(endpoint, json=json, headers=self.headers) + + def put(self, endpoint, json={}): + return self.s.put(endpoint, json=json, headers=self.headers) + def get(self, endpoint, json={}): return self.s.get(endpoint, json=json, headers=self.headers) def set_token(self,token): - self.headers = {"Authorization": f"Bearer {token}"} + self.headers = {"Authorization": f"Bearer {token}"} def unset_token(self): self.headers = {} @@ -72,62 +78,57 @@ class FiregexAPI: def reset(self, delete: bool): self.s.post(f"{self.address}api/reset", json={"delete":delete}) - #Netfilter regex - def nf_get_stats(self): - req = self.s.get(f"{self.address}api/nfregex/stats") - return req.json() - def nf_get_services(self): req = self.s.get(f"{self.address}api/nfregex/services") return req.json() def nf_get_service(self,service_id: str): - req = self.s.get(f"{self.address}api/nfregex/service/{service_id}") + req = self.s.get(f"{self.address}api/nfregex/services/{service_id}") return req.json() def nf_stop_service(self,service_id: str): - req = self.s.get(f"{self.address}api/nfregex/service/{service_id}/stop") + req = self.s.post(f"{self.address}api/nfregex/services/{service_id}/stop") return verify(req) def nf_start_service(self,service_id: str): - req = self.s.get(f"{self.address}api/nfregex/service/{service_id}/start") + req = self.s.post(f"{self.address}api/nfregex/services/{service_id}/start") return verify(req) def nf_delete_service(self,service_id: str): - req = self.s.get(f"{self.address}api/nfregex/service/{service_id}/delete") + req = self.s.delete(f"{self.address}api/nfregex/services/{service_id}") return verify(req) def nf_rename_service(self,service_id: str, newname: str): - req = self.s.post(f"{self.address}api/nfregex/service/{service_id}/rename" , json={"name":newname}) + req = self.s.put(f"{self.address}api/nfregex/services/{service_id}/rename" , json={"name":newname}) return verify(req) def nf_get_service_regexes(self,service_id: str): - req = self.s.get(f"{self.address}api/nfregex/service/{service_id}/regexes") + req = self.s.get(f"{self.address}api/nfregex/services/{service_id}/regexes") return req.json() def nf_get_regex(self,regex_id: str): - req = self.s.get(f"{self.address}api/nfregex/regex/{regex_id}") + req = self.s.get(f"{self.address}api/nfregex/regexes/{regex_id}") return req.json() def nf_delete_regex(self,regex_id: str): - req = self.s.get(f"{self.address}api/nfregex/regex/{regex_id}/delete") + req = self.s.delete(f"{self.address}api/nfregex/regexes/{regex_id}") return verify(req) def nf_enable_regex(self,regex_id: str): - req = self.s.get(f"{self.address}api/nfregex/regex/{regex_id}/enable") + req = self.s.post(f"{self.address}api/nfregex/regexes/{regex_id}/enable") return verify(req) def nf_disable_regex(self,regex_id: str): - req = self.s.get(f"{self.address}api/nfregex/regex/{regex_id}/disable") + req = self.s.post(f"{self.address}api/nfregex/regexes/{regex_id}/disable") return verify(req) def nf_add_regex(self, service_id: str, regex: str, mode: str, active: bool, is_case_sensitive: bool): - req = self.s.post(f"{self.address}api/nfregex/regexes/add", + req = self.s.post(f"{self.address}api/nfregex/regexes", json={"service_id": service_id, "regex": regex, "mode": mode, "active": active, "is_case_sensitive": is_case_sensitive}) return verify(req) def nf_add_service(self, name: str, port: int, proto: str, ip_int: str): - req = self.s.post(f"{self.address}api/nfregex/services/add" , + req = self.s.post(f"{self.address}api/nfregex/services" , json={"name":name,"port":port, "proto": proto, "ip_int": ip_int}) return req.json()["service_id"] if verify(req) else False @@ -137,30 +138,30 @@ class FiregexAPI: return req.json() def ph_get_service(self,service_id: str): - req = self.s.get(f"{self.address}api/porthijack/service/{service_id}") + req = self.s.get(f"{self.address}api/porthijack/services/{service_id}") return req.json() def ph_stop_service(self,service_id: str): - req = self.s.get(f"{self.address}api/porthijack/service/{service_id}/stop") + req = self.s.post(f"{self.address}api/porthijack/services/{service_id}/stop") return verify(req) def ph_start_service(self,service_id: str): - req = self.s.get(f"{self.address}api/porthijack/service/{service_id}/start") + req = self.s.post(f"{self.address}api/porthijack/services/{service_id}/start") return verify(req) def ph_delete_service(self,service_id: str): - req = self.s.get(f"{self.address}api/porthijack/service/{service_id}/delete") + req = self.s.delete(f"{self.address}api/porthijack/services/{service_id}") return verify(req) def ph_rename_service(self,service_id: str,newname: str): - req = self.s.post(f"{self.address}api/porthijack/service/{service_id}/rename" , json={"name":newname}) + req = self.s.put(f"{self.address}api/porthijack/services/{service_id}/rename" , json={"name":newname}) return verify(req) def ph_change_destination(self,service_id: str, ip_dst:string , proxy_port: int): - req = self.s.post(f"{self.address}api/porthijack/service/{service_id}/change-destination", json={"ip_dst": ip_dst, "proxy_port": proxy_port}) + req = self.s.put(f"{self.address}api/porthijack/services/{service_id}/change-destination", json={"ip_dst": ip_dst, "proxy_port": proxy_port}) return verify(req) def ph_add_service(self, name: str, public_port: int, proxy_port: int, proto: str, ip_src: str, ip_dst: str): - req = self.s.post(f"{self.address}api/porthijack/services/add" , + req = self.s.post(f"{self.address}api/porthijack/services" , json={"name":name, "public_port": public_port, "proxy_port":proxy_port, "proto": proto, "ip_src": ip_src, "ip_dst": ip_dst}) return req.json()["service_id"] if verify(req) else False