From 25d2cfd562a5b24f528bd7b020c6909a592ca004 Mon Sep 17 00:00:00 2001 From: DomySh Date: Fri, 12 Aug 2022 11:55:07 +0000 Subject: [PATCH] Port hijack changes and start writing frontend --- backend/modules/nfregex/models.py | 20 ++- backend/modules/nfregex/nftables.py | 10 +- backend/modules/porthijack/firewall.py | 4 +- backend/modules/porthijack/models.py | 16 +- backend/modules/porthijack/nftables.py | 35 ++-- backend/routers/porthijack.py | 58 +++++-- backend/utils/__init__.py | 17 +- backend/utils/loader.py | 2 +- frontend/src/App.tsx | 6 +- frontend/src/components/NavBar/index.tsx | 2 +- .../components/PortHijack/AddNewService.tsx | 155 +++++++++++++++++ .../PortHijack/ServiceRow/RenameForm.tsx | 68 ++++++++ .../PortHijack/ServiceRow/index.module.scss | 18 ++ .../PortHijack/ServiceRow/index.tsx | 156 ++++++++++++++++++ frontend/src/components/PortHijack/utils.ts | 64 +++++++ .../ServiceDetails.tsx | 0 .../pages/{NFRegex.tsx => NFRegex}/index.tsx | 0 frontend/src/pages/PortHijack/index.tsx | 78 +++++++++ 18 files changed, 653 insertions(+), 56 deletions(-) create mode 100755 frontend/src/components/PortHijack/AddNewService.tsx create mode 100644 frontend/src/components/PortHijack/ServiceRow/RenameForm.tsx create mode 100755 frontend/src/components/PortHijack/ServiceRow/index.module.scss create mode 100755 frontend/src/components/PortHijack/ServiceRow/index.tsx create mode 100755 frontend/src/components/PortHijack/utils.ts rename frontend/src/pages/{NFRegex.tsx => NFRegex}/ServiceDetails.tsx (100%) rename frontend/src/pages/{NFRegex.tsx => NFRegex}/index.tsx (100%) create mode 100755 frontend/src/pages/PortHijack/index.tsx diff --git a/backend/modules/nfregex/models.py b/backend/modules/nfregex/models.py index d365412..e020d87 100644 --- a/backend/modules/nfregex/models.py +++ b/backend/modules/nfregex/models.py @@ -11,7 +11,14 @@ class Service: @classmethod def from_dict(cls, var: dict): - return cls(id=var["service_id"], status=var["status"], port=var["port"], name=var["name"], proto=var["proto"], ip_int=var["ip_int"]) + return cls( + id=var["service_id"], + status=var["status"], + port=var["port"], + name=var["name"], + proto=var["proto"], + ip_int=var["ip_int"] + ) class Regex: @@ -27,4 +34,13 @@ class Regex: @classmethod def from_dict(cls, var: dict): - return cls(id=var["regex_id"], regex=base64.b64decode(var["regex"]), mode=var["mode"], service_id=var["service_id"], is_blacklist=var["is_blacklist"], blocked_packets=var["blocked_packets"], is_case_sensitive=var["is_case_sensitive"], active=var["active"]) \ No newline at end of file + return cls( + id=var["regex_id"], + regex=base64.b64decode(var["regex"]), + mode=var["mode"], + service_id=var["service_id"], + is_blacklist=var["is_blacklist"], + blocked_packets=var["blocked_packets"], + is_case_sensitive=var["is_case_sensitive"], + active=var["active"] + ) \ No newline at end of file diff --git a/backend/modules/nfregex/nftables.py b/backend/modules/nfregex/nftables.py index a0a31a0..47b77c2 100644 --- a/backend/modules/nfregex/nftables.py +++ b/backend/modules/nfregex/nftables.py @@ -1,6 +1,6 @@ from typing import List from modules.nfregex.models import Service -from utils import ip_parse, ip_family, NFTableManager +from utils import ip_parse, ip_family, NFTableManager, nftables_int_to_json class FiregexFilter: def __init__(self, proto:str, port:int, ip_int:str, target:str, id:int): @@ -52,10 +52,6 @@ class FiregexTables(NFTableManager): for ele in self.get(): if ele.__eq__(srv): return - - ip_int = ip_parse(srv.ip_int) - ip_addr = str(ip_int).split("/")[0] - ip_addr_cidr = int(str(ip_int).split("/")[1]) init, end = queue_range_output if init > end: init, end = end, init @@ -64,7 +60,7 @@ class FiregexTables(NFTableManager): "table": self.table_name, "chain": self.output_chain, "expr": [ - {'match': {'left': {'payload': {'protocol': ip_family(ip_int), 'field': 'saddr'}}, 'op': '==', 'right': {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}}}, + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'saddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.port)}}, {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} ] @@ -77,7 +73,7 @@ class FiregexTables(NFTableManager): "table": self.table_name, "chain": self.input_chain, "expr": [ - {'match': {'left': {'payload': {'protocol': ip_family(ip_int), 'field': 'daddr'}}, 'op': '==', 'right': {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}}}, + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_int), 'field': 'daddr'}}, 'op': '==', 'right': nftables_int_to_json(srv.ip_int)}}, {'match': {"left": { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(srv.port)}}, {"queue": {"num": str(init) if init == end else {"range":[init, end] }, "flags": ["bypass"]}} ] diff --git a/backend/modules/porthijack/firewall.py b/backend/modules/porthijack/firewall.py index 7325d32..b1d21a4 100644 --- a/backend/modules/porthijack/firewall.py +++ b/backend/modules/porthijack/firewall.py @@ -64,8 +64,8 @@ class ServiceManager: nft.delete(self.srv) self._set_status(False) - async def change_port(self, new_port): - self.srv.proxy_port = new_port + async def refresh(self, srv:Service): + self.srv = srv if self.active: await self.restart() def _set_status(self,active): diff --git a/backend/modules/porthijack/models.py b/backend/modules/porthijack/models.py index ff39882..a89e6d6 100644 --- a/backend/modules/porthijack/models.py +++ b/backend/modules/porthijack/models.py @@ -1,13 +1,23 @@ class Service: - def __init__(self, service_id: str, active: bool, public_port: int, proxy_port: int, name: str, proto: str, ip_int: str): + def __init__(self, service_id: str, active: bool, public_port: int, proxy_port: int, name: str, proto: str, ip_src: str, ip_dst:str): self.service_id = service_id self.active = active self.public_port = public_port self.proxy_port = proxy_port self.name = name self.proto = proto - self.ip_int = ip_int + self.ip_src = ip_src + self.ip_dst = ip_dst @classmethod def from_dict(cls, var: dict): - return cls(service_id=var["service_id"], active=var["active"], public_port=var["public_port"], proxy_port=var["proxy_port"], name=var["name"], proto=var["proto"], ip_int=var["ip_int"]) + return cls( + service_id=var["service_id"], + active=var["active"], + public_port=var["public_port"], + proxy_port=var["proxy_port"], + name=var["name"], + proto=var["proto"], + ip_src=var["ip_src"], + ip_dst=var["ip_dst"] + ) diff --git a/backend/modules/porthijack/nftables.py b/backend/modules/porthijack/nftables.py index 17f0a5a..92255f3 100644 --- a/backend/modules/porthijack/nftables.py +++ b/backend/modules/porthijack/nftables.py @@ -1,21 +1,22 @@ from typing import List from modules.porthijack.models import Service -from utils import ip_parse, ip_family, NFTableManager +from utils import addr_parse, ip_parse, ip_family, NFTableManager, nftables_json_to_int class FiregexHijackRule(): - def __init__(self, proto:str, public_port:int,proxy_port:int, ip_int:str, target:str, id:int): + def __init__(self, proto:str, public_port:int,proxy_port:int, ip_src:str, ip_dst:str, target:str, id:int): self.id = id self.target = target self.proto = proto self.public_port = public_port self.proxy_port = proxy_port - self.ip_int = str(ip_int) + self.ip_src = str(ip_src) + self.ip_dst = str(ip_dst) def __eq__(self, o: object) -> bool: if isinstance(o, FiregexHijackRule): - return self.public_port == o.public_port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int) + return self.public_port == o.public_port and self.proto == o.proto and ip_parse(self.ip_src) == ip_parse(o.ip_src) elif isinstance(o, Service): - return self.public_port == o.public_port and self.proto == o.proto and ip_parse(self.ip_int) == ip_parse(o.ip_int) + return self.public_port == o.public_port and self.proto == o.proto and ip_parse(self.ip_src) == ip_parse(o.ip_src) return False class FiregexTables(NFTableManager): @@ -54,17 +55,15 @@ class FiregexTables(NFTableManager): for ele in self.get(): if ele.__eq__(srv): return - ip_int = ip_parse(srv.ip_int) - ip_addr = str(ip_int).split("/")[0] - ip_addr_cidr = int(str(ip_int).split("/")[1]) self.cmd({ "insert":{ "rule": { "family": "inet", "table": self.table_name, "chain": self.prerouting_porthijack, "expr": [ - {'match': {'left': {'payload': {'protocol': ip_family(ip_int), 'field': 'daddr'}}, 'op': '==', 'right': {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}}}, - {'match': {'left': { "payload": {"protocol": str(srv.proto), "field": "dport"}}, "op": "==", "right": int(srv.public_port)}}, - {'mangle': {'key': {'payload': {'protocol': str(srv.proto), 'field': 'dport'}}, 'value': int(srv.proxy_port)}} + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_src), 'field': 'daddr'}}, 'op': '==', 'right': addr_parse(srv.ip_src)}}, + {'match': {'left': {'payload': {'protocol': str(srv.proto), 'field': 'dport'}}, 'op': '==', 'right': int(srv.public_port)}}, + {'mangle': {'key': {'payload': {'protocol': str(srv.proto), 'field': 'dport'}}, 'value': int(srv.proxy_port)}}, + {'mangle': {'key': {'payload': {'protocol': ip_family(srv.ip_src), 'field': 'daddr'}}, 'value': addr_parse(srv.ip_dst)}} ] }}}) self.cmd({ "insert":{ "rule": { @@ -72,9 +71,10 @@ class FiregexTables(NFTableManager): "table": self.table_name, "chain": self.postrouting_porthijack, "expr": [ - {'match': {'left': {'payload': {'protocol': ip_family(ip_int), 'field': 'saddr'}}, 'op': '==', 'right': {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}}}, + {'match': {'left': {'payload': {'protocol': ip_family(srv.ip_dst), 'field': 'saddr'}}, 'op': '==', 'right': addr_parse(srv.ip_dst)}}, {'match': {'left': { "payload": {"protocol": str(srv.proto), "field": "sport"}}, "op": "==", "right": int(srv.proxy_port)}}, - {'mangle': {'key': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'value': int(srv.public_port)}} + {'mangle': {'key': {'payload': {'protocol': str(srv.proto), 'field': 'sport'}}, 'value': int(srv.public_port)}}, + {'mangle': {'key': {'payload': {'protocol': ip_family(srv.ip_dst), 'field': 'saddr'}}, 'value': addr_parse(srv.ip_src)}} ] }}}) @@ -82,18 +82,15 @@ class FiregexTables(NFTableManager): def get(self) -> List[FiregexHijackRule]: res = [] for filter in self.list_rules(tables=[self.table_name], chains=[self.prerouting_porthijack,self.postrouting_porthijack]): - ip_int = None - if isinstance(filter["expr"][0]["match"]["right"],str): - ip_int = str(ip_parse(filter["expr"][0]["match"]["right"])) - else: - ip_int = f'{filter["expr"][0]["match"]["right"]["prefix"]["addr"]}/{filter["expr"][0]["match"]["right"]["prefix"]["len"]}' + filter["expr"][0]["match"]["right"] res.append(FiregexHijackRule( target=filter["chain"], id=int(filter["handle"]), proto=filter["expr"][1]["match"]["left"]["payload"]["protocol"], public_port=filter["expr"][1]["match"]["right"] if filter["chain"] == self.prerouting_porthijack else filter["expr"][2]["mangle"]["value"], proxy_port=filter["expr"][1]["match"]["right"] if filter["chain"] == self.postrouting_porthijack else filter["expr"][2]["mangle"]["value"], - ip_int=ip_int + ip_src=nftables_json_to_int(filter["expr"][0]["match"]["right"]) if filter["chain"] == self.prerouting_porthijack else nftables_json_to_int(filter["expr"][3]["mangle"]["value"]), + ip_dst=nftables_json_to_int(filter["expr"][0]["match"]["right"]) if filter["chain"] == self.postrouting_porthijack else nftables_json_to_int(filter["expr"][3]["mangle"]["value"]), )) return res diff --git a/backend/routers/porthijack.py b/backend/routers/porthijack.py index 7d66e8f..16b92ba 100644 --- a/backend/routers/porthijack.py +++ b/backend/routers/porthijack.py @@ -3,8 +3,9 @@ import sqlite3 from typing import List, Union from fastapi import APIRouter, HTTPException from pydantic import BaseModel +from modules.porthijack.models import Service from utils.sqlite import SQLite -from utils import ip_parse, refactor_name, refresh_frontend +from utils import addr_parse, ip_family, refactor_name, refresh_frontend from utils.models import ResetRequest, StatusMessageModel from modules.porthijack.nftables import FiregexTables from modules.porthijack.firewall import FirewallManager @@ -16,7 +17,8 @@ class ServiceModel(BaseModel): proxy_port: int name: str proto: str - ip_int: str + ip_src: str + ip_dst: str class RenameForm(BaseModel): name:str @@ -26,7 +28,8 @@ class ServiceAddForm(BaseModel): public_port: int proxy_port: int proto: str - ip_int: str + ip_src: str + ip_dst: str class ServiceAddResponse(BaseModel): status:str @@ -41,15 +44,15 @@ db = SQLite('db/port-hijacking.db', { 'services': { 'service_id': 'VARCHAR(100) PRIMARY KEY', 'active' : 'BOOLEAN NOT NULL CHECK (active IN (0, 1))', - 'public_port': 'INT NOT NULL CHECK(public_port > 0 and public_port < 65536) UNIQUE', + 'public_port': 'INT NOT NULL CHECK(public_port > 0 and public_port < 65536)', 'proxy_port': 'INT NOT NULL CHECK(proxy_port > 0 and proxy_port < 65536 and proxy_port != public_port)', 'name': 'VARCHAR(100) NOT NULL UNIQUE', 'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "udp"))', - 'ip_int': 'VARCHAR(100) NOT NULL', + 'ip_src': 'VARCHAR(100) NOT NULL', + 'ip_dst': 'VARCHAR(100) NOT NULL', }, 'QUERY':[ - "CREATE UNIQUE INDEX IF NOT EXISTS unique_services ON services (public_port, ip_int, proto);", - "" + "CREATE UNIQUE INDEX IF NOT EXISTS unique_services ON services (public_port, ip_src, proto);" ] }) @@ -96,12 +99,12 @@ async def get_general_stats(): @app.get('/services', response_model=List[ServiceModel]) 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_int FROM 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) 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_int FROM services WHERE service_id = ?;", service_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) if len(res) == 0: raise HTTPException(status_code=400, detail="This service does not exists!") return res[0] @@ -139,17 +142,30 @@ async def service_rename(service_id: str, form: RenameForm): await refresh_frontend() return {'status': 'ok'} -class ChangePortRequest(BaseModel): +class ChangeDestination(BaseModel): + ip_dst: str proxy_port: int -@app.post('/service/{service_id}/changeport', response_model=StatusMessageModel) -async def service_changeport(service_id: str, form: ChangePortRequest): - """Request to change the proxy port of a specific service""" +@app.post('/service/{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""" + try: - db.query('UPDATE services SET proxy_port=? WHERE service_id = ?;', form.proxy_port, service_id) + form.ip_dst = addr_parse(form.ip_dst) + except ValueError: + return {"status":"Invalid address"} + srv = Service.from_dict(db.query('SELECT * FROM services WHERE service_id = ?;', service_id)[0]) + if ip_family(form.ip_dst) != ip_family(srv.ip_src): + return {'status': 'The destination ip is not of the same family as the source ip'} + try: + db.query('UPDATE services SET proxy_port=?, ip_dst=? WHERE service_id = ?;', form.proxy_port, form.ip_dst, service_id) except sqlite3.IntegrityError: return {'status': 'Invalid proxy port or service'} - await firewall.get(service_id).change_port(form.proxy_port) + + srv.ip_dst = form.ip_dst + srv.proxy_port = form.proxy_port + await firewall.get(service_id).refresh(srv) + await refresh_frontend() return {'status': 'ok'} @@ -157,18 +173,24 @@ async def service_changeport(service_id: str, form: ChangePortRequest): async def add_new_service(form: ServiceAddForm): """Add a new service""" try: - form.ip_int = ip_parse(form.ip_int) + form.ip_src = addr_parse(form.ip_src) + form.ip_dst = addr_parse(form.ip_dst) except ValueError: return {"status":"Invalid address"} + + if ip_family(form.ip_dst) != ip_family(form.ip_src): + return {"status":"Destination and source addresses must be of the same family"} if form.proto not in ["tcp", "udp"]: return {"status":"Invalid protocol"} + srv_id = None try: srv_id = gen_service_id() - db.query("INSERT INTO services (service_id, active, public_port, proxy_port, name, proto, ip_int) VALUES (?, ?, ?, ?, ?, ?, ?)", - srv_id, False, form.public_port, form.proxy_port , form.name, form.proto, form.ip_int) + db.query("INSERT INTO services (service_id, active, public_port, proxy_port, name, proto, ip_src, ip_dst) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + srv_id, False, form.public_port, form.proxy_port , form.name, form.proto, form.ip_src, form.ip_dst) except sqlite3.IntegrityError: return {'status': 'This type of service already exists'} + await firewall.reload() await refresh_frontend() return {'status': 'ok', 'service_id': srv_id} diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py index 0a4c30d..052be3f 100755 --- a/backend/utils/__init__.py +++ b/backend/utils/__init__.py @@ -1,5 +1,5 @@ import asyncio -from ipaddress import ip_interface +from ipaddress import ip_address, ip_interface import os, socket, psutil, sys, nftables from fastapi_socketio import SocketManager @@ -37,6 +37,9 @@ def list_files(mypath): def ip_parse(ip:str): return str(ip_interface(ip).network) +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" @@ -48,6 +51,18 @@ def get_interfaces(): yield {"name": int_name, "addr":interf.address} return list(_get_interfaces()) +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}} + +def nftables_json_to_int(ip_json_int): + if isinstance(ip_json_int,str): + return str(ip_parse(ip_json_int)) + else: + return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}' + class Singleton(object): __instance = None def __new__(class_, *args, **kwargs): diff --git a/backend/utils/loader.py b/backend/utils/loader.py index f10e3b3..d60c217 100644 --- a/backend/utils/loader.py +++ b/backend/utils/loader.py @@ -90,7 +90,7 @@ 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}") + app.include_router(router.router, prefix=f"/{router.name}", tags=[router.name]) if router.reset: resets.append(router.reset) if router.startup: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5863c83..64ed9e8 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,11 +6,12 @@ import { Outlet, Route, Routes } from 'react-router-dom'; import MainLayout from './components/MainLayout'; import { PasswordSend, ServerStatusResponse } from './js/models'; import { errorNotify, fireUpdateRequest, getstatus, HomeRedirector, login, setpassword } from './js/utils'; -import NFRegex from './pages/NFRegex.tsx'; +import NFRegex from './pages/NFRegex'; import io from 'socket.io-client'; import RegexProxy from './pages/RegexProxy'; -import ServiceDetailsNFRegex from './pages/NFRegex.tsx/ServiceDetails'; +import ServiceDetailsNFRegex from './pages/NFRegex/ServiceDetails'; import ServiceDetailsProxyRegex from './pages/RegexProxy/ServiceDetails'; +import PortHijack from './pages/PortHijack'; const socket = io({transports: ["websocket", "polling"], path:"/sock" }); @@ -153,6 +154,7 @@ function App() { } > } /> + } /> } /> diff --git a/frontend/src/components/NavBar/index.tsx b/frontend/src/components/NavBar/index.tsx index 17bd1db..26e0ff2 100644 --- a/frontend/src/components/NavBar/index.tsx +++ b/frontend/src/components/NavBar/index.tsx @@ -42,7 +42,7 @@ export default function NavBar({ closeNav, opened }: {closeNav: () => void, open } /> } /> - } disabled/> + } /> diff --git a/frontend/src/components/PortHijack/AddNewService.tsx b/frontend/src/components/PortHijack/AddNewService.tsx new file mode 100755 index 0000000..832ea99 --- /dev/null +++ b/frontend/src/components/PortHijack/AddNewService.tsx @@ -0,0 +1,155 @@ +import { Button, Group, NumberInput, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Autocomplete, AutocompleteItem } from '@mantine/core'; +import { useForm } from '@mantine/hooks'; +import React, { useEffect, useState } from 'react'; +import { okNotify, regex_ipv4, regex_ipv6, getipinterfaces } from '../../js/utils'; +import { ImCross } from "react-icons/im" +import { porthijack } from './utils'; + +type ServiceAddForm = { + name:string, + public_port:number, + proxy_port:number, + proto:string, + ip_int:string, + autostart: boolean, +} + +interface ItemProps extends AutocompleteItem { + label: string; +} + +const AutoCompleteItem = React.forwardRef( + ({ label, value, ...props }: ItemProps, ref) =>
+ ( {label} ) -{">"} {value} +
+ ); + +function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) { + + const form = useForm({ + initialValues: { + name:"", + public_port:80, + proxy_port:8080, + proto:"tcp", + ip_int:"", + autostart: true, + }, + validationRules:{ + name: (value) => value !== ""?true:false, + public_port: (value) => value>0 && value<65536, + proxy_port: (value) => value>0 && value<65536, + proto: (value) => ["tcp","udp"].includes(value), + ip_int: (value) => value.match(regex_ipv6)?true:false || value.match(regex_ipv4)?true:false + } + }) + + const [ipInterfaces, setIpInterfaces] = useState([]); + + useEffect(()=>{ + getipinterfaces().then(data => { + setIpInterfaces(data.map(item => ({label:item.name, value:item.addr}))); + }) + },[]) + + const close = () =>{ + onClose() + form.reset() + setError(null) + } + + const [submitLoading, setSubmitLoading] = useState(false) + const [error, setError] = useState(null) + + const submitRequest = ({ name, proxy_port, public_port, autostart, proto, ip_int }:ServiceAddForm) =>{ + setSubmitLoading(true) + porthijack.servicesadd({name, proxy_port, public_port, proto, ip_int }).then( res => { + if (res.status === "ok" && res.service_id){ + setSubmitLoading(false) + close(); + if (autostart) porthijack.servicestart(res.service_id) + okNotify(`Service ${name} has been added`, `Successfully added service from port ${public_port} to ${proxy_port}`) + }else{ + setSubmitLoading(false) + setError("Invalid request! [ "+res.status+" ]") + } + }).catch( err => { + setSubmitLoading(false) + setError("Request Failed! [ "+err+" ]") + }) + } + + + return +
+ + +
+ + + : + + +
+ + + + + + + + +
+ +
+ +
+ + + + + + + + {error?<> + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + :null} + + +
+ +} + +export default AddNewService; diff --git a/frontend/src/components/PortHijack/ServiceRow/RenameForm.tsx b/frontend/src/components/PortHijack/ServiceRow/RenameForm.tsx new file mode 100644 index 0000000..07dd300 --- /dev/null +++ b/frontend/src/components/PortHijack/ServiceRow/RenameForm.tsx @@ -0,0 +1,68 @@ +import { Button, Group, Space, TextInput, Notification, Modal } from '@mantine/core'; +import { useForm } from '@mantine/hooks'; +import React, { useEffect, useState } from 'react'; +import { okNotify } from '../../../js/utils'; +import { ImCross } from "react-icons/im" +import { porthijack, Service } from '../utils'; + +function RenameForm({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:Service }) { + + const form = useForm({ + initialValues: { name:service.name }, + validationRules:{ name: (value) => value !== "" } + }) + + const close = () =>{ + onClose() + form.reset() + setError(null) + } + + useEffect(()=> form.setFieldValue("name", service.name),[opened]) + + const [submitLoading, setSubmitLoading] = useState(false) + const [error, setError] = useState(null) + + const submitRequest = ({ name }:{ name:string }) => { + setSubmitLoading(true) + porthijack.servicerename(service.service_id, name).then( res => { + if (!res){ + setSubmitLoading(false) + close(); + okNotify(`Service ${service.name} has been renamed in ${ name }`, `Successfully renamed service on port ${service.port}`) + }else{ + setSubmitLoading(false) + setError("Error: [ "+res+" ]") + } + }).catch( err => { + setSubmitLoading(false) + setError("Request Failed! [ "+err+" ]") + }) + + } + + + return +
+ + + + + + + + {error?<> + } color="red" onClose={()=>{setError(null)}}> + Error: {error} + :null} + + +
+ +} + +export default RenameForm; diff --git a/frontend/src/components/PortHijack/ServiceRow/index.module.scss b/frontend/src/components/PortHijack/ServiceRow/index.module.scss new file mode 100755 index 0000000..0d91313 --- /dev/null +++ b/frontend/src/components/PortHijack/ServiceRow/index.module.scss @@ -0,0 +1,18 @@ + +@use "../../../index.scss" as *; + +.row{ + width: 95%; + padding: 30px 0px; + border-radius: 20px; + margin: 10px; + @extend .center-flex; +} + +.name{ + font-size: 2.3em; + font-weight: bolder; + margin-right: 10px; + margin-bottom: 13px; + color:#FFF; +} \ No newline at end of file diff --git a/frontend/src/components/PortHijack/ServiceRow/index.tsx b/frontend/src/components/PortHijack/ServiceRow/index.tsx new file mode 100755 index 0000000..c8214d2 --- /dev/null +++ b/frontend/src/components/PortHijack/ServiceRow/index.tsx @@ -0,0 +1,156 @@ +import { ActionIcon, Badge, Divider, Grid, MediaQuery, Menu, Space, Title, Tooltip } from '@mantine/core'; +import React, { useState } from 'react'; +import { FaPlay, FaStop } from 'react-icons/fa'; +import { porthijack, Service } from '../utils'; +import { MdOutlineArrowForwardIos } from "react-icons/md" +import style from "./index.module.scss"; +import YesNoModal from '../../YesNoModal'; +import { errorNotify, okNotify, regex_ipv4 } from '../../../js/utils'; +import { BsTrashFill } from 'react-icons/bs'; +import { BiRename } from 'react-icons/bi' +import RenameForm from './RenameForm'; + +function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) { + + let status_color = service.active ? "teal": "red" + + const [buttonLoading, setButtonLoading] = useState(false) + const [tooltipStopOpened, setTooltipStopOpened] = useState(false); + const [deleteModal, setDeleteModal] = useState(false) + const [renameModal, setRenameModal] = useState(false) + + const stopService = async () => { + setButtonLoading(true) + + await porthijack.servicestop(service.service_id).then(res => { + if(!res){ + okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`) + }else{ + errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`) + }) + setButtonLoading(false); + } + + const startService = async () => { + setButtonLoading(true) + await porthijack.servicestart(service.service_id).then(res => { + if(!res){ + okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`) + }else{ + errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`) + } + }).catch(err => { + errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`) + }) + setButtonLoading(false) + } + + const deleteService = () => { + porthijack.servicedelete(service.service_id).then(res => { + if (!res){ + okNotify("Service delete complete!",`The service ${service.name} has been deleted!`) + }else + errorNotify("An error occurred while deleting a service",`Error: ${res}`) + }).catch(err => { + errorNotify("An error occurred while deleting a service",`Error: ${err}`) + }) + + } + + return <> + + +
+
+
{service.name} :{service.port}
+ Status: {service.active?"ENABLED":"DISABLED"} +
+
+
+
+
{service.name} :{service.port}
+ Status: {service.active?"ENABLED":"DISABLED"} + +
+
+ + + + +
+ + + +
+ + + <> + + +
+ {service.ip_int} on {service.proto} +
+ +
+ + + <> + +
+ + Rename service + } onClick={()=>setRenameModal(true)}>Change service name + + Danger zone + } onClick={()=>setDeleteModal(true)}>Delete Service + + + + setTooltipStopOpened(false)} onBlur={() => setTooltipStopOpened(false)} + onMouseEnter={() => setTooltipStopOpened(true)} onMouseLeave={() => setTooltipStopOpened(false)}> + + + + + + + + + +
+ + {onClick?
+ + +
:null} + + <> + + + + +
+ setDeleteModal(false) } + action={deleteService} + opened={deleteModal} + /> + setRenameModal(false)} + opened={renameModal} + service={service} + /> + +} + +export default ServiceRow; diff --git a/frontend/src/components/PortHijack/utils.ts b/frontend/src/components/PortHijack/utils.ts new file mode 100755 index 0000000..9bd2234 --- /dev/null +++ b/frontend/src/components/PortHijack/utils.ts @@ -0,0 +1,64 @@ +import { ServerResponse } from "../../js/models" +import { getapi, postapi } from "../../js/utils" + +export type GeneralStats = { + services:number +} + +export type Service = { + name:string, + service_id:string, + active:boolean, + port:number, + proto: string, + ip_int: string, + proxy_port: number, + public_port: number, +} + +export type ServiceAddForm = { + name:string, + public_port:number, + proxy_port:number, + proto:string, + ip_int:string, +} + +export type ServiceAddResponse = { + status: string, + service_id?: string, +} + +export const porthijack = { + stats: async () => { + return await getapi("porthijack/stats") as GeneralStats; + }, + services: async () => { + return await getapi("porthijack/services") as Service[]; + }, + serviceinfo: async (service_id:string) => { + return await getapi(`porthijack/service/${service_id}`) as Service; + }, + servicestart: async (service_id:string) => { + const { status } = await getapi(`porthijack/service/${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; + return status === "ok"?undefined:status + }, + servicestop: async (service_id:string) => { + const { status } = await getapi(`porthijack/service/${service_id}/stop`) as ServerResponse; + return status === "ok"?undefined:status + }, + servicesadd: async (data:ServiceAddForm) => { + return await postapi("porthijack/services/add",data) as ServiceAddResponse; + }, + servicedelete: async (service_id:string) => { + const { status } = await getapi(`porthijack/service/${service_id}/delete`) as ServerResponse; + return status === "ok"?undefined:status + }, + changeport: async (service_id:string, proxy_port:number) => { + return await postapi(`porthijack/service/${service_id}/changeport`, {proxy_port}) as ServerResponse; + } +} \ No newline at end of file diff --git a/frontend/src/pages/NFRegex.tsx/ServiceDetails.tsx b/frontend/src/pages/NFRegex/ServiceDetails.tsx similarity index 100% rename from frontend/src/pages/NFRegex.tsx/ServiceDetails.tsx rename to frontend/src/pages/NFRegex/ServiceDetails.tsx diff --git a/frontend/src/pages/NFRegex.tsx/index.tsx b/frontend/src/pages/NFRegex/index.tsx similarity index 100% rename from frontend/src/pages/NFRegex.tsx/index.tsx rename to frontend/src/pages/NFRegex/index.tsx diff --git a/frontend/src/pages/PortHijack/index.tsx b/frontend/src/pages/PortHijack/index.tsx new file mode 100755 index 0000000..3712d32 --- /dev/null +++ b/frontend/src/pages/PortHijack/index.tsx @@ -0,0 +1,78 @@ +import { ActionIcon, Badge, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core'; +import React, { useEffect, useState } from 'react'; +import { BsPlusLg } from "react-icons/bs"; +import { useNavigate } from 'react-router-dom'; +import ServiceRow from '../../components/PortHijack/ServiceRow'; +import { GeneralStats, porthijack, Service } from '../../components/PortHijack/utils'; +import { errorNotify, eventUpdateName, fireUpdateRequest } from '../../js/utils'; +import AddNewService from '../../components/PortHijack/AddNewService'; +import { useWindowEvent } from '@mantine/hooks'; + + +function PortHijack() { + + const [services, setServices] = useState([]); + const [loader, setLoader] = useState(true); + const navigator = useNavigate() + const [open, setOpen] = useState(false); + const [tooltipAddServOpened, setTooltipAddServOpened] = useState(false); + const [tooltipAddOpened, setTooltipAddOpened] = useState(false); + + const [generalStats, setGeneralStats] = useState({services:0}); + const updateInfo = async () => { + + await Promise.all([ + porthijack.stats().then(res => { + setGeneralStats(res) + }).catch( + err => errorNotify("General Info Auto-Update failed!", err.toString()) + ), + porthijack.services().then(res => { + setServices(res) + }).catch(err => { + errorNotify("Home Page Auto-Update failed!", err.toString()) + }) + ]) + setLoader(false) + } + + useWindowEvent(eventUpdateName, updateInfo) + useEffect(fireUpdateRequest,[]) + + const closeModal = () => {setOpen(false);} + + return <> + +
+ Hijack port to proxy +
+ Services: {generalStats.services} + + + setOpen(true)} size="lg" radius="md" variant="filled" + onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)} + onMouseEnter={() => setTooltipAddOpened(true)} onMouseLeave={() => setTooltipAddOpened(false)}> + +
+
+ + {services.length > 0?services.map( srv => { + navigator("/nfregex/"+srv.service_id) + }} />):<> No services found! Add one clicking the "+" buttons + +
+ + setOpen(true)} size="xl" radius="md" variant="filled" + onFocus={() => setTooltipAddServOpened(false)} onBlur={() => setTooltipAddServOpened(false)} + onMouseEnter={() => setTooltipAddServOpened(true)} onMouseLeave={() => setTooltipAddServOpened(false)}> + +
+ } + +
+ + + +} + +export default PortHijack;