Port hijack changes and start writing frontend

This commit is contained in:
DomySh
2022-08-12 11:55:07 +00:00
parent 0193f2efc8
commit 25d2cfd562
18 changed files with 653 additions and 56 deletions

View File

@@ -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"])
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"]
)

View File

@@ -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):
@@ -53,10 +53,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
self.cmd({ "insert":{ "rule": {
@@ -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"]}}
]

View File

@@ -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):

View File

@@ -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"]
)

View File

@@ -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

View File

@@ -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}

View File

@@ -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):

View File

@@ -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:

View File

@@ -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() {
<Route path="regexproxy" element={<RegexProxy><Outlet /></RegexProxy>} >
<Route path=":srv" element={<ServiceDetailsProxyRegex />} />
</Route>
<Route path="porthijack" element={<PortHijack />} />
<Route path="*" element={<HomeRedirector />} />
</Route>
</Routes>

View File

@@ -42,7 +42,7 @@ export default function NavBar({ closeNav, opened }: {closeNav: () => void, open
<Navbar.Section grow component={ScrollArea} px="xs" mt="xs">
<NavBarButton navigate="nfregex" closeNav={closeNav} name="Netfilter Regex" color="blue" icon={<IoMdGitNetwork />} />
<NavBarButton navigate="regexproxy" closeNav={closeNav} name="TCP Proxy Regex Filter" color="lime" icon={<MdTransform />} />
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="red" icon={<GrDirections />} disabled/>
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="red" icon={<GrDirections />} />
</Navbar.Section>
</Navbar>

View File

@@ -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<HTMLDivElement, ItemProps>(
({ label, value, ...props }: ItemProps, ref) => <div ref={ref} {...props}>
( <b>{label}</b> ) -{">"} <b>{value}</b>
</div>
);
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<AutocompleteItem[]>([]);
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<string|null>(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 <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
<TextInput
label="Service name"
placeholder="Challenge 01"
{...form.getInputProps('name')}
/>
<Space h="md" />
<div className='center-flex' style={{width:"100%"}}>
<Autocomplete
label="Public IP Interface (ipv4/ipv6 + CIDR allowed)"
placeholder="10.1.1.0/24"
itemComponent={AutoCompleteItem}
data={ipInterfaces}
{...form.getInputProps('ip_int')}
/>
<Space w="sm" />:<Space w="sm" />
<NumberInput
placeholder="80"
min={1}
max={65535}
label="Public Service port"
{...form.getInputProps('public_port')}
/>
</div>
<Space h="md" />
<NumberInput
placeholder="8080"
min={1}
max={65535}
label="Redirect to port"
{...form.getInputProps('proxy_port')}
/>
<Space h="md" />
<div className='center-flex'>
<Switch
label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })}
/>
<div className="flex-spacer"></div>
<SegmentedControl
data={[
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
]}
{...form.getInputProps('proto')}
/>
</div>
<Group position="right" mt="md">
<Button loading={submitLoading} type="submit">Add Service</Button>
</Group>
<Space h="md" />
{error?<>
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" /></>:null}
</form>
</Modal>
}
export default AddNewService;

View File

@@ -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<string|null>(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 <Modal size="xl" title={`Rename '${service.name}' service on port ${service.port}`} opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
<TextInput
label="Service Name"
placeholder="Awesome Service Name!"
{...form.getInputProps('name')}
/>
<Group position="right" mt="md">
<Button loading={submitLoading} type="submit">Rename</Button>
</Group>
<Space h="md" />
{error?<>
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" /></>:null}
</form>
</Modal>
}
export default RenameForm;

View File

@@ -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;
}

View File

@@ -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 <>
<Grid className={style.row} justify="flex-end" style={{width:"100%"}}>
<Grid.Col md={4} xs={12}>
<MediaQuery smallerThan="md" styles={{ display: 'none' }}><div>
<div className="center-flex-row">
<div className="center-flex"><Title className={style.name}>{service.name}</Title> <Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient">:{service.port}</Badge></div>
<Badge color={status_color} radius="sm" size="lg" variant="filled">Status: <u>{service.active?"ENABLED":"DISABLED"}</u></Badge>
</div>
</div></MediaQuery>
<MediaQuery largerThan="md" styles={{ display: 'none' }}><div>
<div className="center-flex">
<div className="center-flex"><Title className={style.name}>{service.name}</Title> <Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient">:{service.port}</Badge></div>
<Badge style={{marginLeft:"20px"}} color={status_color} radius="sm" size="lg" variant="filled">Status: <u>{service.active?"ENABLED":"DISABLED"}</u></Badge>
<Space w="xl" />
</div>
</div></MediaQuery>
<MediaQuery largerThan="md" styles={{ display: 'none' }}>
<Space h="xl" />
</MediaQuery>
</Grid.Col>
<Grid.Col className="center-flex" md={8} xs={12}>
<MediaQuery smallerThan="md" styles={{ display: 'none' }}>
<div className='flex-spacer' />
</MediaQuery>
<MediaQuery largerThan="md" styles={{ display: 'none' }}>
<><Space w="xl" /><Space w="xl" /></>
</MediaQuery>
<div className="center-flex-row">
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
</div>
<MediaQuery largerThan="md" styles={{ display: 'none' }}>
<div className='flex-spacer' />
</MediaQuery>
<MediaQuery smallerThan="md" styles={{ display: 'none' }}>
<><Space w="xl" /><Space w="xl" /></>
</MediaQuery>
<div className="center-flex">
<Menu>
<Menu.Label><b>Rename service</b></Menu.Label>
<Menu.Item icon={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" icon={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</Menu>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} transition="pop" transitionDuration={200} transitionTimingFunction="ease" color="red" opened={tooltipStopOpened} tooltipId="tooltip-stop-id">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={!service.active}
aria-describedby="tooltip-stop-id"
onFocus={() => setTooltipStopOpened(false)} onBlur={() => setTooltipStopOpened(false)}
onMouseEnter={() => setTooltipStopOpened(true)} onMouseLeave={() => setTooltipStopOpened(false)}>
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" transition="pop" zIndex={0} transitionDuration={200} transitionTimingFunction="ease" color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={service.active}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
</div>
<Space w="xl" /><Space w="xl" />
{onClick?<div>
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={45} />
<Space w="xl" />
</div>:null}
<MediaQuery largerThan="md" styles={{ display: 'none' }}>
<><Space w="xl" /><Space w="xl" /></>
</MediaQuery>
</Grid.Col>
</Grid>
<hr style={{width:"100%"}}/>
<YesNoModal
title='Are you sure to delete this service?'
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
onClose={()=>setDeleteModal(false) }
action={deleteService}
opened={deleteModal}
/>
<RenameForm
onClose={()=>setRenameModal(false)}
opened={renameModal}
service={service}
/>
</>
}
export default ServiceRow;

View File

@@ -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;
}
}

View File

@@ -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<Service[]>([]);
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<GeneralStats>({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 <>
<Space h="sm" />
<div className='center-flex'>
<Title order={4}>Hijack port to proxy</Title>
<div className='flex-spacer' />
<Badge size="sm" color="yellow" variant="filled">Services: {generalStats.services}</Badge>
<Space w="xs" />
<Tooltip label="Add a new service" position='bottom' transition="pop" transitionDuration={200} transitionTimingFunction="ease" color="blue" opened={tooltipAddOpened}>
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"
onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)}
onMouseEnter={() => setTooltipAddOpened(true)} onMouseLeave={() => setTooltipAddOpened(false)}><BsPlusLg size={18} /></ActionIcon>
</Tooltip>
</div>
<div id="service-list" className="center-flex-row">
<LoadingOverlay visible={loader} />
{services.length > 0?services.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
navigator("/nfregex/"+srv.service_id)
}} />):<><Space h="xl"/> <Title className='center-flex' align='center' order={3}>No services found! Add one clicking the "+" buttons</Title>
<Space h="xl" /> <Space h="xl" /> <Space h="xl" /> <Space h="xl" />
<div className='center-flex'>
<Tooltip label="Add a new service" transition="pop" transitionDuration={200} transitionTimingFunction="ease" color="blue" opened={tooltipAddServOpened}>
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
onFocus={() => setTooltipAddServOpened(false)} onBlur={() => setTooltipAddServOpened(false)}
onMouseEnter={() => setTooltipAddServOpened(true)} onMouseLeave={() => setTooltipAddServOpened(false)}><BsPlusLg size="20px" /></ActionIcon>
</Tooltip>
</div>
</>}
<AddNewService opened={open} onClose={closeModal} />
</div>
<AddNewService opened={open} onClose={closeModal} />
</>
}
export default PortHijack;