This commit is contained in:
Your Name
2025-12-08 01:41:08 +03:00
parent 16f96aa6f6
commit 9af3023a37
49 changed files with 4609 additions and 4020 deletions

View File

@@ -13,6 +13,8 @@ import { Firewall } from './pages/Firewall';
import { useQueryClient } from '@tanstack/react-query';
import NFProxy from './pages/NFProxy';
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
import TrafficViewer from './pages/NFProxy/TrafficViewer';
import TrafficViewerMain from './pages/TrafficViewer';
import { useAuthStore } from './js/store';
function App() {
@@ -172,7 +174,9 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
</Route>
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
<Route path=":srv" element={<ServiceDetailsNFProxy />} />
<Route path=":srv/traffic" element={<TrafficViewer />} />
</Route>
<Route path="traffic" element={<TrafficViewerMain />} />
<Route path="firewall" element={<Firewall />} />
<Route path="porthijack" element={<PortHijack />} />
<Route path="*" element={<HomeRedirector />} />

View File

@@ -1,115 +1,115 @@
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useState } from 'react';
import { RegexAddForm } from '../js/models';
import { b64decode, b64encode, okNotify } from '../js/utils';
import { ImCross } from "react-icons/im"
import { nfregex } from './NFRegex/utils';
type RegexAddInfo = {
regex:string,
mode:string,
is_case_insensitive:boolean,
deactive:boolean
}
function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:string }) {
const form = useForm({
initialValues: {
regex:"",
mode:"C",
is_case_insensitive:false,
deactive:false
},
validate:{
regex: (value) => value !== "" ? null : "Regex is required",
mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode",
}
})
const close = () =>{
onClose()
form.reset()
setError(null)
}
const [submitLoading, setSubmitLoading] = useState(false)
const [error, setError] = useState<string|null>(null)
const submitRequest = (values:RegexAddInfo) => {
setSubmitLoading(true)
const request:RegexAddForm = {
is_case_sensitive: !values.is_case_insensitive,
service_id: service,
mode: values.mode?values.mode:"B",
regex: b64encode(values.regex),
active: !values.deactive
}
setSubmitLoading(false)
nfregex.regexesadd(request).then( res => {
if (!res){
setSubmitLoading(false)
close();
okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`)
}else if (res.toLowerCase() === "invalid regex"){
setSubmitLoading(false)
form.setFieldError("regex", "Invalid Regex")
}else{
setSubmitLoading(false)
setError("Error: [ "+res+" ]")
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}
return <Modal size="xl" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
<TextInput
label="Regex"
placeholder="[A-Z0-9]{31}="
{...form.getInputProps('regex')}
/>
<Space h="md" />
<Switch
label="Case insensitive"
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
/>
<Space h="md" />
<Switch
label="Deactivate"
{...form.getInputProps('deactive', { type: 'checkbox' })}
/>
<Space h="md" />
<Select
data={[
{ value: 'C', label: 'Client -> Server' },
{ value: 'S', label: 'Server -> Client' },
{ value: 'B', label: 'Both (Client <-> Server)' },
]}
label="Choose the source of the packets to filter"
variant="filled"
{...form.getInputProps('mode')}
/>
<Group align="right" mt="md">
<Button loading={submitLoading} type="submit">Add Filter</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 AddNewRegex;
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useState } from 'react';
import { RegexAddForm } from '../js/models';
import { b64decode, b64encode, okNotify } from '../js/utils';
import { ImCross } from "react-icons/im"
import { nfregex } from './NFRegex/utils';
type RegexAddInfo = {
regex:string,
mode:string,
is_case_insensitive:boolean,
deactive:boolean
}
function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:string }) {
const form = useForm({
initialValues: {
regex:"",
mode:"C",
is_case_insensitive:false,
deactive:false
},
validate:{
regex: (value) => value !== "" ? null : "Regex is required",
mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode",
}
})
const close = () =>{
onClose()
form.reset()
setError(null)
}
const [submitLoading, setSubmitLoading] = useState(false)
const [error, setError] = useState<string|null>(null)
const submitRequest = (values:RegexAddInfo) => {
setSubmitLoading(true)
const request:RegexAddForm = {
is_case_sensitive: !values.is_case_insensitive,
service_id: service,
mode: values.mode?values.mode:"B",
regex: b64encode(values.regex),
active: !values.deactive
}
setSubmitLoading(false)
nfregex.regexesadd(request).then( res => {
if (!res){
setSubmitLoading(false)
close();
okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`)
}else if (res.toLowerCase() === "invalid regex"){
setSubmitLoading(false)
form.setFieldError("regex", "Invalid Regex")
}else{
setSubmitLoading(false)
setError("Error: [ "+res+" ]")
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}
return <Modal size="xl" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
<TextInput
label="Regex"
placeholder="[A-Z0-9]{31}="
{...form.getInputProps('regex')}
/>
<Space h="md" />
<Switch
label="Case insensitive"
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
/>
<Space h="md" />
<Switch
label="Deactivate"
{...form.getInputProps('deactive', { type: 'checkbox' })}
/>
<Space h="md" />
<Select
data={[
{ value: 'C', label: 'Client -> Server' },
{ value: 'S', label: 'Server -> Client' },
{ value: 'B', label: 'Both (Client <-> Server)' },
]}
label="Choose the source of the packets to filter"
variant="filled"
{...form.getInputProps('mode')}
/>
<Group align="right" mt="md">
<Button loading={submitLoading} type="submit">Add Filter</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 AddNewRegex;

View File

@@ -1,82 +1,82 @@
import React, { useState } from 'react';
import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core';
import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils';
import { AiFillHome } from "react-icons/ai"
import { useNavigate } from 'react-router';
import { FaLock } from 'react-icons/fa';
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
import { ImExit } from 'react-icons/im';
import ResetPasswordModal from './ResetPasswordModal';
import ResetModal from './ResetModal';
import { MenuDropDownWithButton } from '../MainLayout';
import { useNavbarStore } from '../../js/store';
function HeaderPage(props: any) {
const navigator = useNavigate()
const { navOpened, toggleNav } = useNavbarStore()
const logout_action = () => {
logout().then(r => {
window.location.reload()
}).catch(r => {
errorNotify("Logout failed!",`Error: ${r}`)
})
}
const go_to_home = () => {
navigator(`/${getMainPath()}`)
}
const [changePasswordModal, setChangePasswordModal] = useState(false);
const [resetFiregexModal, setResetFiregexModal] = useState(false);
return <AppShell.Header className="firegex__header__header" {...props}>
<Burger
hiddenFrom='md'
ml="lg"
opened={navOpened}
className="firegex__header__navbtn"
onClick={toggleNav}
size="sm"
/>
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center"}} ml={5}>
<Box className="firegex__header__divlogo">
<Tooltip zIndex={0} label="Home" openDelay={1000} color="dark" position="right" >
<Image src="/header-logo.png" alt="Firegex logo" w={50} onClick={()=>navigator("/")}/>
</Tooltip>
</Box>
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
<Title order={2} >[Fi]*regex</Title>
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a></p>
</Box>
</Box>
<Box className="flex-spacer" />
<MenuDropDownWithButton>
<Menu.Label>Firewall Access</Menu.Label>
<Menu.Item leftSection={<FaLock size={14} />} onClick={() => setChangePasswordModal(true)}>Change Password</Menu.Item>
<Divider />
<Menu.Label>Actions</Menu.Label>
<Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item>
</MenuDropDownWithButton>
<Space w="md" />
<Tooltip label="Home" position='bottom' color="teal">
<ActionIcon color="teal" style={{marginRight:"10px"}}
size="xl" radius="md" variant="filled"
onClick={go_to_home}>
<AiFillHome size="25px" />
</ActionIcon>
</Tooltip>
<Tooltip label="Logout" position='bottom' color="blue">
<ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled">
<ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
</Tooltip>
<ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} />
<ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} />
<Space w="xl" />
</AppShell.Header>
}
export default HeaderPage;
import React, { useState } from 'react';
import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core';
import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils';
import { AiFillHome } from "react-icons/ai"
import { useNavigate } from 'react-router';
import { FaLock } from 'react-icons/fa';
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
import { ImExit } from 'react-icons/im';
import ResetPasswordModal from './ResetPasswordModal';
import ResetModal from './ResetModal';
import { MenuDropDownWithButton } from '../MainLayout';
import { useNavbarStore } from '../../js/store';
function HeaderPage(props: any) {
const navigator = useNavigate()
const { navOpened, toggleNav } = useNavbarStore()
const logout_action = () => {
logout().then(r => {
window.location.reload()
}).catch(r => {
errorNotify("Logout failed!",`Error: ${r}`)
})
}
const go_to_home = () => {
navigator(`/${getMainPath()}`)
}
const [changePasswordModal, setChangePasswordModal] = useState(false);
const [resetFiregexModal, setResetFiregexModal] = useState(false);
return <AppShell.Header className="firegex__header__header" {...props}>
<Burger
hiddenFrom='md'
ml="lg"
opened={navOpened}
className="firegex__header__navbtn"
onClick={toggleNav}
size="sm"
/>
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center"}} ml={5}>
<Box className="firegex__header__divlogo">
<Tooltip zIndex={0} label="Home" openDelay={1000} color="dark" position="right" >
<Image src="/header-logo.png" alt="Firegex logo" w={50} onClick={()=>navigator("/")}/>
</Tooltip>
</Box>
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
<Title order={2} >[Fi]*regex</Title>
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a></p>
</Box>
</Box>
<Box className="flex-spacer" />
<MenuDropDownWithButton>
<Menu.Label>Firewall Access</Menu.Label>
<Menu.Item leftSection={<FaLock size={14} />} onClick={() => setChangePasswordModal(true)}>Change Password</Menu.Item>
<Divider />
<Menu.Label>Actions</Menu.Label>
<Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item>
</MenuDropDownWithButton>
<Space w="md" />
<Tooltip label="Home" position='bottom' color="teal">
<ActionIcon color="teal" style={{marginRight:"10px"}}
size="xl" radius="md" variant="filled"
onClick={go_to_home}>
<AiFillHome size="25px" />
</ActionIcon>
</Tooltip>
<Tooltip label="Logout" position='bottom' color="blue">
<ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled">
<ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
</Tooltip>
<ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} />
<ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} />
<Space w="xl" />
</AppShell.Header>
}
export default HeaderPage;

View File

@@ -1,51 +1,51 @@
import { useEffect } from 'react';
import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core';
import { AppShell } from '@mantine/core';
import NavBar from './NavBar';
import HeaderPage from './Header';
import { getMainPath } from '../js/utils';
import { useLocation } from 'react-router';
import { useNavbarStore } from '../js/store';
import { HiMenu } from "react-icons/hi";
function MainLayout({ children }:{ children:any }) {
const { navOpened } = useNavbarStore()
const location = useLocation()
useEffect(()=>{
if (location.pathname !== "/"){
sessionStorage.setItem('home_section', getMainPath())
}
},[location.pathname])
return <AppShell
header={{ height: 70 }}
navbar={{ width: 300 , breakpoint: "md", collapsed: { mobile: !navOpened } }}
p="md"
>
<HeaderPage />
<NavBar />
<AppShell.Main>
<Container size="lg">
{children}
</Container>
</AppShell.Main>
<Space h="lg" />
</AppShell>
}
export default MainLayout;
export const MenuDropDownWithButton = ({children}:{children:any}) => <Menu withArrow>
<Menu.Target>
<Tooltip label="More options" color="gray">
<ActionIcon variant='transparent'>
<HiMenu size={24} color='#FFF'/>
</ActionIcon>
</Tooltip>
</Menu.Target>
<Menu.Dropdown>
{children}
</Menu.Dropdown>
</Menu>
import { useEffect } from 'react';
import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core';
import { AppShell } from '@mantine/core';
import NavBar from './NavBar';
import HeaderPage from './Header';
import { getMainPath } from '../js/utils';
import { useLocation } from 'react-router';
import { useNavbarStore } from '../js/store';
import { HiMenu } from "react-icons/hi";
function MainLayout({ children }:{ children:any }) {
const { navOpened } = useNavbarStore()
const location = useLocation()
useEffect(()=>{
if (location.pathname !== "/"){
sessionStorage.setItem('home_section', getMainPath())
}
},[location.pathname])
return <AppShell
header={{ height: 70 }}
navbar={{ width: 300 , breakpoint: "md", collapsed: { mobile: !navOpened } }}
p="md"
>
<HeaderPage />
<NavBar />
<AppShell.Main>
<Container size="lg">
{children}
</Container>
</AppShell.Main>
<Space h="lg" />
</AppShell>
}
export default MainLayout;
export const MenuDropDownWithButton = ({children}:{children:any}) => <Menu withArrow>
<Menu.Target>
<Tooltip label="More options" color="gray">
<ActionIcon variant='transparent'>
<HiMenu size={24} color='#FFF'/>
</ActionIcon>
</Tooltip>
</Menu.Target>
<Menu.Dropdown>
{children}
</Menu.Dropdown>
</Menu>

View File

@@ -1,139 +1,139 @@
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react';
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
import { ImCross } from "react-icons/im"
import { nfproxy, Service } from './utils';
import PortAndInterface from '../PortAndInterface';
import { IoMdInformationCircleOutline } from "react-icons/io";
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
const initialValues = {
name: "",
port:edit?.port??8080,
ip_int:edit?.ip_int??"",
proto:edit?.proto??"tcp",
fail_open: edit?.fail_open??false,
autostart: true
}
const form = useForm({
initialValues: initialValues,
validate:{
name: (value) => edit? null : value !== "" ? null : "Service name is required",
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
}
})
useEffect(() => {
if (opened){
form.setInitialValues(initialValues)
form.reset()
}
}, [opened])
const close = () =>{
onClose()
form.reset()
setError(null)
}
const [submitLoading, setSubmitLoading] = useState(false)
const [error, setError] = useState<string|null>(null)
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
setSubmitLoading(true)
if (edit){
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
if (!res){
setSubmitLoading(false)
close();
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}else{
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
if (res.status === "ok" && res.service_id){
setSubmitLoading(false)
close();
if (autostart) nfproxy.servicestart(res.service_id)
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
}else{
setSubmitLoading(false)
setError("Invalid request! [ "+res.status+" ]")
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}
}
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
{!edit?<TextInput
label="Service name"
placeholder="Challenge 01"
{...form.getInputProps('name')}
/>:null}
<Space h="md" />
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
<Space h="md" />
<Box className='center-flex'>
<Box>
{!edit?<Switch
label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })}
/>:null}
<Space h="sm" />
<Switch
label={<Box className='center-flex'>
Enable fail-open nfqueue
<Space w="xs" />
<Tooltip label={<>
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
</>}>
<IoMdInformationCircleOutline size={15} />
</Tooltip>
</Box>}
{...form.getInputProps('fail_open', { type: 'checkbox' })}
/>
</Box>
<Box className="flex-spacer"></Box>
{edit?null:<SegmentedControl
data={[
{ label: 'TCP', value: 'tcp' },
{ label: 'HTTP', value: 'http' },
]}
{...form.getInputProps('proto')}
/>}
</Box>
<Group justify='flex-end' mt="md" mb="sm">
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
</Group>
{error?<>
<Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" />
</>:null}
</form>
</Modal>
}
export default AddEditService;
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react';
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
import { ImCross } from "react-icons/im"
import { nfproxy, Service } from './utils';
import PortAndInterface from '../PortAndInterface';
import { IoMdInformationCircleOutline } from "react-icons/io";
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
const initialValues = {
name: "",
port:edit?.port??8080,
ip_int:edit?.ip_int??"",
proto:edit?.proto??"tcp",
fail_open: edit?.fail_open??false,
autostart: true
}
const form = useForm({
initialValues: initialValues,
validate:{
name: (value) => edit? null : value !== "" ? null : "Service name is required",
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
}
})
useEffect(() => {
if (opened){
form.setInitialValues(initialValues)
form.reset()
}
}, [opened])
const close = () =>{
onClose()
form.reset()
setError(null)
}
const [submitLoading, setSubmitLoading] = useState(false)
const [error, setError] = useState<string|null>(null)
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
setSubmitLoading(true)
if (edit){
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
if (!res){
setSubmitLoading(false)
close();
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}else{
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
if (res.status === "ok" && res.service_id){
setSubmitLoading(false)
close();
if (autostart) nfproxy.servicestart(res.service_id)
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
}else{
setSubmitLoading(false)
setError("Invalid request! [ "+res.status+" ]")
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}
}
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
{!edit?<TextInput
label="Service name"
placeholder="Challenge 01"
{...form.getInputProps('name')}
/>:null}
<Space h="md" />
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
<Space h="md" />
<Box className='center-flex'>
<Box>
{!edit?<Switch
label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })}
/>:null}
<Space h="sm" />
<Switch
label={<Box className='center-flex'>
Enable fail-open nfqueue
<Space w="xs" />
<Tooltip label={<>
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
</>}>
<IoMdInformationCircleOutline size={15} />
</Tooltip>
</Box>}
{...form.getInputProps('fail_open', { type: 'checkbox' })}
/>
</Box>
<Box className="flex-spacer"></Box>
{edit?null:<SegmentedControl
data={[
{ label: 'TCP', value: 'tcp' },
{ label: 'HTTP', value: 'http' },
]}
{...form.getInputProps('proto')}
/>}
</Box>
<Group justify='flex-end' mt="md" mb="sm">
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
</Group>
{error?<>
<Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" />
</>:null}
</form>
</Modal>
}
export default AddEditService;

View File

@@ -1,164 +1,164 @@
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
import { useState } from 'react';
import { FaPlay, FaStop } from 'react-icons/fa';
import { nfproxy, Service, serviceQueryKey } from '../utils';
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
import YesNoModal from '../../YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from './RenameForm';
import { MenuDropDownWithButton } from '../../MainLayout';
import { useQueryClient } from '@tanstack/react-query';
import { TbPlugConnected } from "react-icons/tb";
import { FaFilter } from "react-icons/fa";
import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../AddEditService';
import { FaPencilAlt } from "react-icons/fa";
import { ExceptionWarning } from '../ExceptionWarning';
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
let status_color = "gray";
switch(service.status){
case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break;
}
const queryClient = useQueryClient()
const [buttonLoading, setButtonLoading] = useState(false)
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const isMedium = isMediumScreen()
const stopService = async () => {
setButtonLoading(true)
await nfproxy.servicestop(service.service_id).then(res => {
if(!res){
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey)
}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 nfproxy.servicestart(service.service_id).then(res => {
if(!res){
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey)
}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 = () => {
nfproxy.servicedelete(service.service_id).then(res => {
if (!res){
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey)
}else
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
})
}
return <>
<Box className='firegex__nfregex__rowbox'>
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
<Box>
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
<MdDoubleArrow size={30} style={{color: "white"}}/>
<Title className="firegex__nfregex__name" ml="xs">
{service.name}
</Title>
</Box>
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
:{service.port}
</Badge>
</Box>
{isMedium?null:<Space w="xl" />}
</Box>
<Box className={isMedium?"center-flex":"center-flex-row"}>
<Box 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>
<Space h="xs" />
<Box className='center-flex'>
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
<Space w="xs" />
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
</Box>
</Box>
{isMedium?<Space w="xl" />:<Space h="lg" />}
<Box className="center-flex">
<ExceptionWarning service_id={service.service_id} />
<Space w="sm"/>
<MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={service.status === "stop"}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
{isMedium?<Space w="xl" />:<Space w="md" />}
{onClick?<Box className='firegex__service_forward_btn'>
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
</Box>:null}
</Box>
</Box>
</Box>
</Box>
<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 filters associated. This will cause the shutdown of your service! ⚠️`}
onClose={()=>setDeleteModal(false) }
action={deleteService}
opened={deleteModal}
/>
<RenameForm
onClose={()=>setRenameModal(false)}
opened={renameModal}
service={service}
/>
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={service}
/>
</>
}
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
import { useState } from 'react';
import { FaPlay, FaStop } from 'react-icons/fa';
import { nfproxy, Service, serviceQueryKey } from '../utils';
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
import YesNoModal from '../../YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from './RenameForm';
import { MenuDropDownWithButton } from '../../MainLayout';
import { useQueryClient } from '@tanstack/react-query';
import { TbPlugConnected } from "react-icons/tb";
import { FaFilter } from "react-icons/fa";
import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../AddEditService';
import { FaPencilAlt } from "react-icons/fa";
import { ExceptionWarning } from '../ExceptionWarning';
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
let status_color = "gray";
switch(service.status){
case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break;
}
const queryClient = useQueryClient()
const [buttonLoading, setButtonLoading] = useState(false)
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const isMedium = isMediumScreen()
const stopService = async () => {
setButtonLoading(true)
await nfproxy.servicestop(service.service_id).then(res => {
if(!res){
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey)
}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 nfproxy.servicestart(service.service_id).then(res => {
if(!res){
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey)
}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 = () => {
nfproxy.servicedelete(service.service_id).then(res => {
if (!res){
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey)
}else
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
})
}
return <>
<Box className='firegex__nfregex__rowbox'>
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
<Box>
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
<MdDoubleArrow size={30} style={{color: "white"}}/>
<Title className="firegex__nfregex__name" ml="xs">
{service.name}
</Title>
</Box>
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
:{service.port}
</Badge>
</Box>
{isMedium?null:<Space w="xl" />}
</Box>
<Box className={isMedium?"center-flex":"center-flex-row"}>
<Box 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>
<Space h="xs" />
<Box className='center-flex'>
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
<Space w="xs" />
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
</Box>
</Box>
{isMedium?<Space w="xl" />:<Space h="lg" />}
<Box className="center-flex">
<ExceptionWarning service_id={service.service_id} />
<Space w="sm"/>
<MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={service.status === "stop"}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
{isMedium?<Space w="xl" />:<Space w="md" />}
{onClick?<Box className='firegex__service_forward_btn'>
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
</Box>:null}
</Box>
</Box>
</Box>
</Box>
<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 filters associated. This will cause the shutdown of your service! ⚠️`}
onClose={()=>setDeleteModal(false) }
action={deleteService}
opened={deleteModal}
/>
<RenameForm
onClose={()=>setRenameModal(false)}
opened={renameModal}
service={service}
/>
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={service}
/>
</>
}

View File

@@ -1,175 +1,182 @@
import { PyFilter, ServerResponse } from "../../js/models"
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
import { useQuery } from "@tanstack/react-query"
export type Service = {
service_id:string,
name:string,
status:string,
port:number,
proto: string,
ip_int: string,
n_filters:number,
edited_packets:number,
blocked_packets:number,
fail_open:boolean,
}
export type ServiceAddForm = {
name:string,
port:number,
proto:string,
ip_int:string,
fail_open: boolean,
}
export type ServiceSettings = {
port?:number,
ip_int?:string,
fail_open?: boolean,
}
export type ServiceAddResponse = {
status: string,
service_id?: string,
}
export const serviceQueryKey = ["nfproxy","services"]
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
queryKey:[...serviceQueryKey,service_id,"pyfilters"],
queryFn:() => nfproxy.servicepyfilters(service_id)
})
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
queryFn:() => nfproxy.getpyfilterscode(service_id)
})
export const nfproxy = {
services: async () => {
return await getapi("nfproxy/services") as Service[];
},
serviceinfo: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}`) as Service;
},
pyfilterenable: async (service_id:string, filter_name:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse;
return status === "ok"?undefined:status
},
pyfilterdisable: async (service_id:string, filter_name:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse;
return status === "ok"?undefined:status
},
servicestart: async (service_id:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
return status === "ok"?undefined:status
},
servicerename: async (service_id:string, name: string) => {
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
return status === "ok"?undefined:status
},
servicestop: async (service_id:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
return status === "ok"?undefined:status
},
servicesadd: async (data:ServiceAddForm) => {
return await postapi("nfproxy/services",data) as ServiceAddResponse;
},
servicedelete: async (service_id:string) => {
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
return status === "ok"?undefined:status
},
servicepyfilters: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
},
settings: async (service_id:string, data:ServiceSettings) => {
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
return status === "ok"?undefined:status
},
getpyfilterscode: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}/code`) as string;
},
setpyfilterscode: async (service_id:string, code:string) => {
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
return status === "ok"?undefined:status
}
}
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
# From here we can import the DataTypes that we want to use:
# The data type must be specified in the filter functions
# And will also interally be used to decide when call some filters and how aggregate data
from firegex.nfproxy.models import RawPacket
# global context in this execution is dedicated to a single TCP stream
# - This code will be executed once at the TCP stream start
# - The filter will be called for each packet in the stream
# - You can store in global context some data you need, but exceeding with data stored could be dangerous
# - At the end of the stream the global context will be destroyed
from firegex.nfproxy import pyfilter
# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
# - The filter must return one of the following values:
# - ACCEPT: The packet will be accepted
# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream)
# - UNSTABLE_MANGLE: The packet will be mangled and accepted
# - DROP: All the packets in this stream will be easly dropped
# If you want, you can use print to debug your filters, but this could slow down the filter
# Filter names must be unique and are specified by the name of the function wrapped by the decorator
@pyfilter
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
def strange_filter(packet:RawPacket):
# Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
# Also is not garanteed that l4_data is the same of the packet data:
# packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
# Unorder packets in TCP are accepted by default, and python is not called in this case
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
if b"TEST_MANGLING" in packet.l4_data:
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
return UNSTABLE_MANGLE
# Drops the traffic
if b"BAD DATA 1" in packet.data:
return DROP
# Rejects the traffic
if b"BAD DATA 2" in packet.data:
return REJECT
# Accepts the traffic (default if None is returned)
return ACCEPT
# Example with a higher level of abstraction
@pyfilter
def http_filter(http:HTTPRequest):
if http.method == "GET" and "test" in http.url:
return REJECT
# ADVANCED OPTIONS
# You can specify some additional options on the streaming managment
# pyproxy will automatically store all the packets (already ordered by the c++ binary):
#
# If the stream is too big, you can specify what actions to take:
# This can be done defining some variables in the global context
# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB)
# NOTE: the stream size is calculated and managed indipendently by the data type handling system
# Only types required by at least 1 filter will be stored.
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic
from firege.nfproxy import FullStreamAction
# Example of a global context
FGEX_STREAM_MAX_SIZE = 4096
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic
`
import { PyFilter, ServerResponse } from "../../js/models"
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
import { useQuery } from "@tanstack/react-query"
export type Service = {
service_id:string,
name:string,
status:string,
port:number,
proto: string,
ip_int: string,
n_filters:number,
edited_packets:number,
blocked_packets:number,
fail_open:boolean,
}
export type ServiceAddForm = {
name:string,
port:number,
proto:string,
ip_int:string,
fail_open: boolean,
}
export type ServiceSettings = {
port?:number,
ip_int?:string,
fail_open?: boolean,
}
export type ServiceAddResponse = {
status: string,
service_id?: string,
}
export const serviceQueryKey = ["nfproxy","services"]
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
queryKey:[...serviceQueryKey,service_id,"pyfilters"],
queryFn:() => nfproxy.servicepyfilters(service_id)
})
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
queryFn:() => nfproxy.getpyfilterscode(service_id)
})
export const nfproxy = {
services: async () => {
return await getapi("nfproxy/services") as Service[];
},
serviceinfo: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}`) as Service;
},
pyfilterenable: async (service_id:string, filter_name:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse;
return status === "ok"?undefined:status
},
pyfilterdisable: async (service_id:string, filter_name:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse;
return status === "ok"?undefined:status
},
servicestart: async (service_id:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
return status === "ok"?undefined:status
},
servicerename: async (service_id:string, name: string) => {
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
return status === "ok"?undefined:status
},
servicestop: async (service_id:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
return status === "ok"?undefined:status
},
servicesadd: async (data:ServiceAddForm) => {
return await postapi("nfproxy/services",data) as ServiceAddResponse;
},
servicedelete: async (service_id:string) => {
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
return status === "ok"?undefined:status
},
servicepyfilters: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
},
settings: async (service_id:string, data:ServiceSettings) => {
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
return status === "ok"?undefined:status
},
getpyfilterscode: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}/code`) as string;
},
setpyfilterscode: async (service_id:string, code:string) => {
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
return status === "ok"?undefined:status
},
gettraffic: async (service_id:string, limit:number = 500) => {
return await getapi(`nfproxy/services/${service_id}/traffic?limit=${limit}`) as { events: any[], count: number };
},
cleartraffic: async (service_id:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/traffic/clear`) as ServerResponse;
return status === "ok"?undefined:status
}
}
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
# From here we can import the DataTypes that we want to use:
# The data type must be specified in the filter functions
# And will also interally be used to decide when call some filters and how aggregate data
from firegex.nfproxy.models import RawPacket
# global context in this execution is dedicated to a single TCP stream
# - This code will be executed once at the TCP stream start
# - The filter will be called for each packet in the stream
# - You can store in global context some data you need, but exceeding with data stored could be dangerous
# - At the end of the stream the global context will be destroyed
from firegex.nfproxy import pyfilter
# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
# - The filter must return one of the following values:
# - ACCEPT: The packet will be accepted
# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream)
# - UNSTABLE_MANGLE: The packet will be mangled and accepted
# - DROP: All the packets in this stream will be easly dropped
# If you want, you can use print to debug your filters, but this could slow down the filter
# Filter names must be unique and are specified by the name of the function wrapped by the decorator
@pyfilter
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
def strange_filter(packet:RawPacket):
# Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
# Also is not garanteed that l4_data is the same of the packet data:
# packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
# Unorder packets in TCP are accepted by default, and python is not called in this case
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
if b"TEST_MANGLING" in packet.l4_data:
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
return UNSTABLE_MANGLE
# Drops the traffic
if b"BAD DATA 1" in packet.data:
return DROP
# Rejects the traffic
if b"BAD DATA 2" in packet.data:
return REJECT
# Accepts the traffic (default if None is returned)
return ACCEPT
# Example with a higher level of abstraction
@pyfilter
def http_filter(http:HTTPRequest):
if http.method == "GET" and "test" in http.url:
return REJECT
# ADVANCED OPTIONS
# You can specify some additional options on the streaming managment
# pyproxy will automatically store all the packets (already ordered by the c++ binary):
#
# If the stream is too big, you can specify what actions to take:
# This can be done defining some variables in the global context
# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB)
# NOTE: the stream size is calculated and managed indipendently by the data type handling system
# Only types required by at least 1 filter will be stored.
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic
from firege.nfproxy import FullStreamAction
# Example of a global context
FGEX_STREAM_MAX_SIZE = 4096
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic
`

View File

@@ -1,139 +1,139 @@
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react';
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
import { ImCross } from "react-icons/im"
import { nfregex, Service } from './utils';
import PortAndInterface from '../PortAndInterface';
import { IoMdInformationCircleOutline } from "react-icons/io";
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
const initialValues = {
name: "",
port:edit?.port??8080,
ip_int:edit?.ip_int??"",
proto:edit?.proto??"tcp",
fail_open: edit?.fail_open??false,
autostart: true
}
const form = useForm({
initialValues: initialValues,
validate:{
name: (value) => edit? null : value !== "" ? null : "Service name is required",
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
}
})
useEffect(() => {
if (opened){
form.setInitialValues(initialValues)
form.reset()
}
}, [opened])
const close = () =>{
onClose()
form.reset()
setError(null)
}
const [submitLoading, setSubmitLoading] = useState(false)
const [error, setError] = useState<string|null>(null)
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
setSubmitLoading(true)
if (edit){
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
if (!res){
setSubmitLoading(false)
close();
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}else{
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
if (res.status === "ok" && res.service_id){
setSubmitLoading(false)
close();
if (autostart) nfregex.servicestart(res.service_id)
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
}else{
setSubmitLoading(false)
setError("Invalid request! [ "+res.status+" ]")
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}
}
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
{!edit?<TextInput
label="Service name"
placeholder="Challenge 01"
{...form.getInputProps('name')}
/>:null}
<Space h="md" />
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
<Space h="md" />
<Box className='center-flex'>
<Box>
{!edit?<Switch
label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })}
/>:null}
<Space h="sm" />
<Switch
label={<Box className='center-flex'>
Enable fail-open nfqueue
<Space w="xs" />
<Tooltip label={<>
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
</>}>
<IoMdInformationCircleOutline size={15} />
</Tooltip>
</Box>}
{...form.getInputProps('fail_open', { type: 'checkbox' })}
/>
</Box>
<Box className="flex-spacer"></Box>
<SegmentedControl
data={[
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
]}
{...form.getInputProps('proto')}
/>
</Box>
<Group justify='flex-end' mt="md" mb="sm">
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
</Group>
{error?<>
<Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" />
</>:null}
</form>
</Modal>
}
export default AddEditService;
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react';
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
import { ImCross } from "react-icons/im"
import { nfregex, Service } from './utils';
import PortAndInterface from '../PortAndInterface';
import { IoMdInformationCircleOutline } from "react-icons/io";
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
const initialValues = {
name: "",
port:edit?.port??8080,
ip_int:edit?.ip_int??"",
proto:edit?.proto??"tcp",
fail_open: edit?.fail_open??false,
autostart: true
}
const form = useForm({
initialValues: initialValues,
validate:{
name: (value) => edit? null : value !== "" ? null : "Service name is required",
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
}
})
useEffect(() => {
if (opened){
form.setInitialValues(initialValues)
form.reset()
}
}, [opened])
const close = () =>{
onClose()
form.reset()
setError(null)
}
const [submitLoading, setSubmitLoading] = useState(false)
const [error, setError] = useState<string|null>(null)
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
setSubmitLoading(true)
if (edit){
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
if (!res){
setSubmitLoading(false)
close();
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}else{
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
if (res.status === "ok" && res.service_id){
setSubmitLoading(false)
close();
if (autostart) nfregex.servicestart(res.service_id)
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
}else{
setSubmitLoading(false)
setError("Invalid request! [ "+res.status+" ]")
}
}).catch( err => {
setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]")
})
}
}
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}>
{!edit?<TextInput
label="Service name"
placeholder="Challenge 01"
{...form.getInputProps('name')}
/>:null}
<Space h="md" />
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
<Space h="md" />
<Box className='center-flex'>
<Box>
{!edit?<Switch
label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })}
/>:null}
<Space h="sm" />
<Switch
label={<Box className='center-flex'>
Enable fail-open nfqueue
<Space w="xs" />
<Tooltip label={<>
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
</>}>
<IoMdInformationCircleOutline size={15} />
</Tooltip>
</Box>}
{...form.getInputProps('fail_open', { type: 'checkbox' })}
/>
</Box>
<Box className="flex-spacer"></Box>
<SegmentedControl
data={[
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
]}
{...form.getInputProps('proto')}
/>
</Box>
<Group justify='flex-end' mt="md" mb="sm">
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
</Group>
{error?<>
<Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" />
</>:null}
</form>
</Modal>
}
export default AddEditService;

View File

@@ -1,158 +1,158 @@
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core';
import { useState } from 'react';
import { FaPlay, FaStop } from 'react-icons/fa';
import { nfregex, Service, serviceQueryKey } from '../utils';
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
import YesNoModal from '../../YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from './RenameForm';
import { MenuDropDownWithButton } from '../../MainLayout';
import { useQueryClient } from '@tanstack/react-query';
import { FaFilter } from "react-icons/fa";
import { VscRegex } from "react-icons/vsc";
import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../AddEditService';
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
let status_color = "gray";
switch(service.status){
case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break;
}
const queryClient = useQueryClient()
const [buttonLoading, setButtonLoading] = useState(false)
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const isMedium = isMediumScreen()
const stopService = async () => {
setButtonLoading(true)
await nfregex.servicestop(service.service_id).then(res => {
if(!res){
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey)
}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 nfregex.servicestart(service.service_id).then(res => {
if(!res){
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey)
}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 = () => {
nfregex.servicedelete(service.service_id).then(res => {
if (!res){
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey)
}else
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
})
}
return <>
<Box className='firegex__nfregex__rowbox'>
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
<Box>
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
<MdDoubleArrow size={30} style={{color: "white"}}/>
<Title className="firegex__nfregex__name" ml="xs">
{service.name}
</Title>
</Box>
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
:{service.port}
</Badge>
</Box>
{isMedium?null:<Space w="xl" />}
</Box>
<Box className={isMedium?"center-flex":"center-flex-row"}>
<Box 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>
<Space h="xs" />
<Box className='center-flex'>
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
</Box>
</Box>
{isMedium?<Space w="xl" />:<Space h="lg" />}
<Box className="center-flex">
<MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={service.status === "stop"}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
{isMedium?<Space w="xl" />:<Space w="md" />}
{onClick?<Box className='firegex__service_forward_btn'>
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
</Box>:null}
</Box>
</Box>
</Box>
</Box>
<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}
/>
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={service}
/>
</>
}
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core';
import { useState } from 'react';
import { FaPlay, FaStop } from 'react-icons/fa';
import { nfregex, Service, serviceQueryKey } from '../utils';
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
import YesNoModal from '../../YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from './RenameForm';
import { MenuDropDownWithButton } from '../../MainLayout';
import { useQueryClient } from '@tanstack/react-query';
import { FaFilter } from "react-icons/fa";
import { VscRegex } from "react-icons/vsc";
import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../AddEditService';
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
let status_color = "gray";
switch(service.status){
case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break;
}
const queryClient = useQueryClient()
const [buttonLoading, setButtonLoading] = useState(false)
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const isMedium = isMediumScreen()
const stopService = async () => {
setButtonLoading(true)
await nfregex.servicestop(service.service_id).then(res => {
if(!res){
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey)
}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 nfregex.servicestart(service.service_id).then(res => {
if(!res){
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey)
}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 = () => {
nfregex.servicedelete(service.service_id).then(res => {
if (!res){
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey)
}else
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
})
}
return <>
<Box className='firegex__nfregex__rowbox'>
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
<Box>
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
<MdDoubleArrow size={30} style={{color: "white"}}/>
<Title className="firegex__nfregex__name" ml="xs">
{service.name}
</Title>
</Box>
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
:{service.port}
</Badge>
</Box>
{isMedium?null:<Space w="xl" />}
</Box>
<Box className={isMedium?"center-flex":"center-flex-row"}>
<Box 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>
<Space h="xs" />
<Box className='center-flex'>
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
</Box>
</Box>
{isMedium?<Space w="xl" />:<Space h="lg" />}
<Box className="center-flex">
<MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={service.status === "stop"}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
{isMedium?<Space w="xl" />:<Space w="md" />}
{onClick?<Box className='firegex__service_forward_btn'>
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
</Box>:null}
</Box>
</Box>
</Box>
</Box>
<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}
/>
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={service}
/>
</>
}

View File

@@ -1,95 +1,95 @@
import { RegexFilter, ServerResponse } from "../../js/models"
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
import { RegexAddForm } from "../../js/models"
import { useQuery, useQueryClient } from "@tanstack/react-query"
export type Service = {
name:string,
service_id:string,
status:string,
port:number,
proto: string,
ip_int: string,
n_packets:number,
n_regex:number,
fail_open:boolean,
}
export type ServiceAddForm = {
name:string,
port:number,
proto:string,
ip_int:string,
fail_open: boolean,
}
export type ServiceSettings = {
port?:number,
proto?:string,
ip_int?:string,
fail_open?: boolean,
}
export type ServiceAddResponse = {
status: string,
service_id?: string,
}
export const serviceQueryKey = ["nfregex","services"]
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
queryKey:[...serviceQueryKey,service_id,"regexes"],
queryFn:() => nfregex.serviceregexes(service_id)
})
export const nfregex = {
services: async () => {
return await getapi("nfregex/services") as Service[];
},
serviceinfo: async (service_id:string) => {
return await getapi(`nfregex/services/${service_id}`) as Service;
},
regexdelete: async (regex_id:number) => {
const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse;
return status === "ok"?undefined:status
},
regexenable: async (regex_id:number) => {
const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse;
return status === "ok"?undefined:status
},
regexdisable: async (regex_id:number) => {
const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse;
return status === "ok"?undefined:status
},
servicestart: async (service_id:string) => {
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 putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse;
return status === "ok"?undefined:status
},
servicestop: async (service_id:string) => {
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",data) as ServiceAddResponse;
},
servicedelete: async (service_id:string) => {
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",data) as ServerResponse;
return status === "ok"?undefined:status
},
serviceregexes: async (service_id:string) => {
return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[];
},
settings: async (service_id:string, data:ServiceSettings) => {
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
return status === "ok"?undefined:status
},
import { RegexFilter, ServerResponse } from "../../js/models"
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
import { RegexAddForm } from "../../js/models"
import { useQuery, useQueryClient } from "@tanstack/react-query"
export type Service = {
name:string,
service_id:string,
status:string,
port:number,
proto: string,
ip_int: string,
n_packets:number,
n_regex:number,
fail_open:boolean,
}
export type ServiceAddForm = {
name:string,
port:number,
proto:string,
ip_int:string,
fail_open: boolean,
}
export type ServiceSettings = {
port?:number,
proto?:string,
ip_int?:string,
fail_open?: boolean,
}
export type ServiceAddResponse = {
status: string,
service_id?: string,
}
export const serviceQueryKey = ["nfregex","services"]
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
queryKey:[...serviceQueryKey,service_id,"regexes"],
queryFn:() => nfregex.serviceregexes(service_id)
})
export const nfregex = {
services: async () => {
return await getapi("nfregex/services") as Service[];
},
serviceinfo: async (service_id:string) => {
return await getapi(`nfregex/services/${service_id}`) as Service;
},
regexdelete: async (regex_id:number) => {
const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse;
return status === "ok"?undefined:status
},
regexenable: async (regex_id:number) => {
const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse;
return status === "ok"?undefined:status
},
regexdisable: async (regex_id:number) => {
const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse;
return status === "ok"?undefined:status
},
servicestart: async (service_id:string) => {
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 putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse;
return status === "ok"?undefined:status
},
servicestop: async (service_id:string) => {
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",data) as ServiceAddResponse;
},
servicedelete: async (service_id:string) => {
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",data) as ServerResponse;
return status === "ok"?undefined:status
},
serviceregexes: async (service_id:string) => {
return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[];
},
settings: async (service_id:string, data:ServiceSettings) => {
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
return status === "ok"?undefined:status
},
}

View File

@@ -7,6 +7,7 @@ import { PiWallLight } from "react-icons/pi";
import { useNavbarStore } from "../../js/store";
import { getMainPath } from "../../js/utils";
import { BsRegex } from "react-icons/bs";
import { MdVisibility } from "react-icons/md";
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
@@ -40,6 +41,7 @@ export default function NavBar() {
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
<NavBarButton navigate="traffic" closeNav={closeNav} name="Traffic Viewer" color="cyan" icon={<MdVisibility size={19} />} />
{/* <Box px="xs" mt="lg">
<Title order={5}>Experimental Features 🧪</Title>
</Box>

View File

@@ -1,113 +1,113 @@
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useState } from 'react';
import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils';
import { ImCross } from "react-icons/im"
import { porthijack } from './utils';
import PortAndInterface from '../PortAndInterface';
type ServiceAddForm = {
name:string,
public_port:number,
proxy_port:number,
proto:string,
ip_src:string,
ip_dst:string,
autostart: boolean,
}
function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) {
const form = useForm({
initialValues: {
name:"",
public_port:80,
proxy_port:8080,
proto:"tcp",
ip_src:"",
ip_dst:"127.0.0.1",
autostart: false,
},
validate:{
name: (value) => value !== ""? null : "Service name is required",
public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port",
proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port",
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
ip_src: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid source IP address",
ip_dst: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid destination IP address",
}
})
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_src, ip_dst }:ServiceAddForm) =>{
setSubmitLoading(true)
porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).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" />
<PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" />
<Space h="md" />
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" />
<Space h="md" />
<Box className='center-flex'>
<Switch
label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })}
/>
<Box className="flex-spacer" />
<SegmentedControl
data={[
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
]}
{...form.getInputProps('proto')}
/>
</Box>
<Group justify='flex-end' mt="md" mb="sm">
<Button loading={submitLoading} type="submit">Add Service</Button>
</Group>
{error?<>
<Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" />
</>:null}
</form>
</Modal>
}
export default AddNewService;
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useState } from 'react';
import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils';
import { ImCross } from "react-icons/im"
import { porthijack } from './utils';
import PortAndInterface from '../PortAndInterface';
type ServiceAddForm = {
name:string,
public_port:number,
proxy_port:number,
proto:string,
ip_src:string,
ip_dst:string,
autostart: boolean,
}
function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) {
const form = useForm({
initialValues: {
name:"",
public_port:80,
proxy_port:8080,
proto:"tcp",
ip_src:"",
ip_dst:"127.0.0.1",
autostart: false,
},
validate:{
name: (value) => value !== ""? null : "Service name is required",
public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port",
proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port",
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
ip_src: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid source IP address",
ip_dst: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid destination IP address",
}
})
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_src, ip_dst }:ServiceAddForm) =>{
setSubmitLoading(true)
porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).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" />
<PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" />
<Space h="md" />
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" />
<Space h="md" />
<Box className='center-flex'>
<Switch
label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })}
/>
<Box className="flex-spacer" />
<SegmentedControl
data={[
{ label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' },
]}
{...form.getInputProps('proto')}
/>
</Box>
<Group justify='flex-end' mt="md" mb="sm">
<Button loading={submitLoading} type="submit">Add Service</Button>
</Group>
{error?<>
<Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error}
</Notification><Space h="md" />
</>:null}
</form>
</Modal>
}
export default AddNewService;

View File

@@ -1,152 +1,152 @@
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
import { useState } from 'react';
import { FaPlay, FaStop } from 'react-icons/fa';
import { porthijack, Service } from '../utils';
import YesNoModal from '../../YesNoModal';
import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils';
import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from './RenameForm';
import ChangeDestination from './ChangeDestination';
import { useForm } from '@mantine/form';
import { MenuDropDownWithButton } from '../../MainLayout';
import { MdDoubleArrow } from "react-icons/md";
export default function ServiceRow({ service }:{ service:Service }) {
let status_color = service.active ? "teal": "red"
const [buttonLoading, setButtonLoading] = useState(false)
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [changeDestModal, setChangeDestModal] = useState(false)
const isMedium = isMediumScreen()
const form = useForm({
initialValues: { proxy_port:service.proxy_port },
validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" }
})
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.public_port} has been stopped!`)
}else{
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the stopping of the service ${service.public_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.public_port} has been started!`)
}else{
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the starting of the service ${service.public_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 <>
<Box className='firegex__nfregex__rowbox'>
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
<Box>
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
<MdDoubleArrow size={30} style={{color: "white"}}/>
<Title className="firegex__nfregex__name" ml="xs">
{service.name}
</Title>
</Box>
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
<Badge color={status_color} radius="md" size="md" variant="filled">{service.active?"ENABLED":"DISABLED"}</Badge>
<Badge color={service.proto === "tcp"?"cyan":"orange"} radius="md" size="md" variant="filled">
{service.proto}
</Badge>
</Box>
{isMedium?null:<Space w="xl" />}
</Box>
<Box className={isMedium?"center-flex":"center-flex-row"}>
<Box className="center-flex-row">
<Badge color="lime" radius="sm" size="lg" variant="filled">
FROM {service.ip_src} :{service.public_port}
</Badge>
<Space h="sm" />
<Badge color="blue" radius="sm" size="lg" variant="filled">
<Box className="center-flex">
TO {service.ip_dst} :{service.proxy_port}
</Box>
</Badge>
</Box>
{isMedium?<Space w="xl" />:<Space h="lg" />}
<Box className="center-flex">
<MenuDropDownWithButton>
<Menu.Label><b>Rename service</b></Menu.Label>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Menu.Label><b>Change destination</b></Menu.Label>
<Menu.Item leftSection={<BsArrowRepeat size={18} />} onClick={()=>setChangeDestModal(true)}>Change hijacking destination</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={!service.active}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={service.active}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
</Box>
</Box>
<YesNoModal
title='Are you sure to delete this service?'
description={`You are going to delete the service '${service.public_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}
/>
<ChangeDestination
onClose={()=>setChangeDestModal(false)}
opened={changeDestModal}
service={service}
/>
</>
}
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
import { useState } from 'react';
import { FaPlay, FaStop } from 'react-icons/fa';
import { porthijack, Service } from '../utils';
import YesNoModal from '../../YesNoModal';
import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils';
import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from './RenameForm';
import ChangeDestination from './ChangeDestination';
import { useForm } from '@mantine/form';
import { MenuDropDownWithButton } from '../../MainLayout';
import { MdDoubleArrow } from "react-icons/md";
export default function ServiceRow({ service }:{ service:Service }) {
let status_color = service.active ? "teal": "red"
const [buttonLoading, setButtonLoading] = useState(false)
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [changeDestModal, setChangeDestModal] = useState(false)
const isMedium = isMediumScreen()
const form = useForm({
initialValues: { proxy_port:service.proxy_port },
validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" }
})
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.public_port} has been stopped!`)
}else{
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the stopping of the service ${service.public_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.public_port} has been started!`)
}else{
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the starting of the service ${service.public_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 <>
<Box className='firegex__nfregex__rowbox'>
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
<Box>
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
<MdDoubleArrow size={30} style={{color: "white"}}/>
<Title className="firegex__nfregex__name" ml="xs">
{service.name}
</Title>
</Box>
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
<Badge color={status_color} radius="md" size="md" variant="filled">{service.active?"ENABLED":"DISABLED"}</Badge>
<Badge color={service.proto === "tcp"?"cyan":"orange"} radius="md" size="md" variant="filled">
{service.proto}
</Badge>
</Box>
{isMedium?null:<Space w="xl" />}
</Box>
<Box className={isMedium?"center-flex":"center-flex-row"}>
<Box className="center-flex-row">
<Badge color="lime" radius="sm" size="lg" variant="filled">
FROM {service.ip_src} :{service.public_port}
</Badge>
<Space h="sm" />
<Badge color="blue" radius="sm" size="lg" variant="filled">
<Box className="center-flex">
TO {service.ip_dst} :{service.proxy_port}
</Box>
</Badge>
</Box>
{isMedium?<Space w="xl" />:<Space h="lg" />}
<Box className="center-flex">
<MenuDropDownWithButton>
<Menu.Label><b>Rename service</b></Menu.Label>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Menu.Label><b>Change destination</b></Menu.Label>
<Menu.Item leftSection={<BsArrowRepeat size={18} />} onClick={()=>setChangeDestModal(true)}>Change hijacking destination</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={!service.active}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={service.active}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
</Box>
</Box>
<YesNoModal
title='Are you sure to delete this service?'
description={`You are going to delete the service '${service.public_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}
/>
<ChangeDestination
onClose={()=>setChangeDestModal(false)}
opened={changeDestModal}
service={service}
/>
</>
}

View File

@@ -1,64 +1,64 @@
import { ServerResponse } from "../../js/models"
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
import { useQuery } from "@tanstack/react-query"
export type GeneralStats = {
services:number
}
export type Service = {
name:string,
service_id:string,
active:boolean,
proto: string,
ip_src: string,
ip_dst: string,
proxy_port: number,
public_port: number,
}
export type ServiceAddForm = {
name:string,
public_port:number,
proxy_port:number,
proto:string,
ip_src: string,
ip_dst: string,
}
export type ServiceAddResponse = ServerResponse & { service_id: string }
export const queryKey = ["porthijack","services"]
export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services})
export const porthijack = {
services: async () : Promise<Service[]> => {
return await getapi("porthijack/services") as Service[];
},
serviceinfo: async (service_id:string) => {
return await getapi(`porthijack/services/${service_id}`) as Service;
},
servicestart: async (service_id:string) => {
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 putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse;
return status === "ok"?undefined:status
},
servicestop: async (service_id:string) => {
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",data) as ServiceAddResponse;
},
servicedelete: async (service_id:string) => {
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 putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse;
}
import { ServerResponse } from "../../js/models"
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
import { useQuery } from "@tanstack/react-query"
export type GeneralStats = {
services:number
}
export type Service = {
name:string,
service_id:string,
active:boolean,
proto: string,
ip_src: string,
ip_dst: string,
proxy_port: number,
public_port: number,
}
export type ServiceAddForm = {
name:string,
public_port:number,
proxy_port:number,
proto:string,
ip_src: string,
ip_dst: string,
}
export type ServiceAddResponse = ServerResponse & { service_id: string }
export const queryKey = ["porthijack","services"]
export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services})
export const porthijack = {
services: async () : Promise<Service[]> => {
return await getapi("porthijack/services") as Service[];
},
serviceinfo: async (service_id:string) => {
return await getapi(`porthijack/services/${service_id}`) as Service;
},
servicestart: async (service_id:string) => {
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 putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse;
return status === "ok"?undefined:status
},
servicestop: async (service_id:string) => {
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",data) as ServiceAddResponse;
},
servicedelete: async (service_id:string) => {
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 putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse;
}
}

View File

@@ -1,44 +1,44 @@
import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
import { useState } from 'react';
import { PyFilter } from '../../js/models';
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
import { FaPause, FaPlay } from 'react-icons/fa';
import { FaFilter } from "react-icons/fa";
import { nfproxy } from '../NFProxy/utils';
import { FaPencilAlt } from 'react-icons/fa';
export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) {
const isMedium = isMediumScreen()
const changeRegexStatus = () => {
(filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => {
if(!res){
okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`)
}else{
errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
}
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
}
return <Box my="sm" display="flex" style={{alignItems:"center"}}>
<Box className="firegex__regexview__pyfilter_text" style={{ width: "100%", alignItems: "center"}} display="flex" >
<Badge size="sm" radius="lg" mr="sm" color={filterInfo.active?"lime":"red"} variant="filled" />
{filterInfo.name}
<Box className='flex-spacer' />
<Space w="xs" />
{isMedium?<>
<Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge>
<Space w="xs" />
<Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge>
<Space w="lg" />
</>:null}
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"}>
<ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="lg" radius="md" variant="filled">
{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
</Tooltip>
</Box>
</Box>
}
import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
import { useState } from 'react';
import { PyFilter } from '../../js/models';
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
import { FaPause, FaPlay } from 'react-icons/fa';
import { FaFilter } from "react-icons/fa";
import { nfproxy } from '../NFProxy/utils';
import { FaPencilAlt } from 'react-icons/fa';
export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) {
const isMedium = isMediumScreen()
const changeRegexStatus = () => {
(filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => {
if(!res){
okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`)
}else{
errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
}
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
}
return <Box my="sm" display="flex" style={{alignItems:"center"}}>
<Box className="firegex__regexview__pyfilter_text" style={{ width: "100%", alignItems: "center"}} display="flex" >
<Badge size="sm" radius="lg" mr="sm" color={filterInfo.active?"lime":"red"} variant="filled" />
{filterInfo.name}
<Box className='flex-spacer' />
<Space w="xs" />
{isMedium?<>
<Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge>
<Space w="xs" />
<Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge>
<Space w="lg" />
</>:null}
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"}>
<ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="lg" radius="md" variant="filled">
{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
</Tooltip>
</Box>
</Box>
}

View File

@@ -1,85 +1,85 @@
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
import { useState } from 'react';
import { RegexFilter } from '../../js/models';
import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
import { BsTrashFill } from "react-icons/bs"
import YesNoModal from '../YesNoModal';
import { FaPause, FaPlay } from 'react-icons/fa';
import { useClipboard } from '@mantine/hooks';
import { FaFilter } from "react-icons/fa";
import { nfregex } from '../NFRegex/utils';
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
const mode_string = regexInfo.mode === "C"? "C -> S":
regexInfo.mode === "S"? "S -> C":
regexInfo.mode === "B"? "C <-> S": "🤔"
let regex_expr = b64decode(regexInfo.regex);
const [deleteModal, setDeleteModal] = useState(false);
const clipboard = useClipboard({ timeout: 500 });
const deleteRegex = () => {
nfregex.regexdelete(regexInfo.id).then(res => {
if(!res){
okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`)
}else{
errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${res}`)
}
}).catch( err => errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${err}`))
}
const changeRegexStatus = () => {
(regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => {
if(!res){
okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`)
}else{
errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
}
}).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
}
return <Box className="firegex__regexview__box">
<Box>
<Box className='center-flex' style={{width: "100%"}}>
<Box className="firegex__regexview__outer_regex_text">
<Text className="firegex__regexview__regex_text" onClick={()=>{
clipboard.copy(regex_expr)
okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`)
}}>{regex_expr}</Text>
</Box>
<Space w="xs" />
<Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"}>
<ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
>{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
</Tooltip>
<Space w="xs" />
<Tooltip label="Delete regex" zIndex={0} color="red" >
<ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled">
<BsTrashFill size={22} /></ActionIcon>
</Tooltip>
</Box>
<Box display="flex" mt="sm" ml="xs">
<Badge size="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {regexInfo.n_packets}</Badge>
<Space w="xs" />
<Badge size="md" color={regexInfo.active?"lime":"red"} variant="filled">{regexInfo.active?"ACTIVE":"DISABLED"}</Badge>
<Space w="xs" />
<Badge size="md" color={regexInfo.is_case_sensitive?"grape":"pink"} variant="filled">{regexInfo.is_case_sensitive?"Strict":"Loose"}</Badge>
<Space w="xs" />
<Badge size="md" color="blue" variant="filled">{mode_string}</Badge>
</Box>
</Box>
<YesNoModal
title='Are you sure to delete this regex?'
description={`You are going to delete the regex '${regex_expr}'.`}
onClose={()=>setDeleteModal(false)}
action={deleteRegex}
opened={deleteModal}
/>
</Box>
}
export default RegexView;
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
import { useState } from 'react';
import { RegexFilter } from '../../js/models';
import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
import { BsTrashFill } from "react-icons/bs"
import YesNoModal from '../YesNoModal';
import { FaPause, FaPlay } from 'react-icons/fa';
import { useClipboard } from '@mantine/hooks';
import { FaFilter } from "react-icons/fa";
import { nfregex } from '../NFRegex/utils';
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
const mode_string = regexInfo.mode === "C"? "C -> S":
regexInfo.mode === "S"? "S -> C":
regexInfo.mode === "B"? "C <-> S": "🤔"
let regex_expr = b64decode(regexInfo.regex);
const [deleteModal, setDeleteModal] = useState(false);
const clipboard = useClipboard({ timeout: 500 });
const deleteRegex = () => {
nfregex.regexdelete(regexInfo.id).then(res => {
if(!res){
okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`)
}else{
errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${res}`)
}
}).catch( err => errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${err}`))
}
const changeRegexStatus = () => {
(regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => {
if(!res){
okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`)
}else{
errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
}
}).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
}
return <Box className="firegex__regexview__box">
<Box>
<Box className='center-flex' style={{width: "100%"}}>
<Box className="firegex__regexview__outer_regex_text">
<Text className="firegex__regexview__regex_text" onClick={()=>{
clipboard.copy(regex_expr)
okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`)
}}>{regex_expr}</Text>
</Box>
<Space w="xs" />
<Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"}>
<ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
>{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
</Tooltip>
<Space w="xs" />
<Tooltip label="Delete regex" zIndex={0} color="red" >
<ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled">
<BsTrashFill size={22} /></ActionIcon>
</Tooltip>
</Box>
<Box display="flex" mt="sm" ml="xs">
<Badge size="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {regexInfo.n_packets}</Badge>
<Space w="xs" />
<Badge size="md" color={regexInfo.active?"lime":"red"} variant="filled">{regexInfo.active?"ACTIVE":"DISABLED"}</Badge>
<Space w="xs" />
<Badge size="md" color={regexInfo.is_case_sensitive?"grape":"pink"} variant="filled">{regexInfo.is_case_sensitive?"Strict":"Loose"}</Badge>
<Space w="xs" />
<Badge size="md" color="blue" variant="filled">{mode_string}</Badge>
</Box>
</Box>
<YesNoModal
title='Are you sure to delete this regex?'
description={`You are going to delete the regex '${regex_expr}'.`}
onClose={()=>setDeleteModal(false)}
action={deleteRegex}
opened={deleteModal}
/>
</Box>
}
export default RegexView;

View File

@@ -1,19 +1,19 @@
import { Button, Group, Modal } from '@mantine/core';
import React from 'react';
function YesNoModal( { title, description, action, onClose, opened}:{ title:string, description:string, onClose:()=>void, action:()=>void, opened:boolean} ){
return <Modal size="xl" title={title} opened={opened} onClose={onClose} centered>
{description}
<Group justify='flex-end' mt="md">
<Button onClick={()=>{
onClose()
action()
}} color="teal" type="submit">Yes</Button>
<Button onClick={onClose} color="red" type="submit">No</Button>
</Group>
</Modal>
}
import { Button, Group, Modal } from '@mantine/core';
import React from 'react';
function YesNoModal( { title, description, action, onClose, opened}:{ title:string, description:string, onClose:()=>void, action:()=>void, opened:boolean} ){
return <Modal size="xl" title={title} opened={opened} onClose={onClose} centered>
{description}
<Group justify='flex-end' mt="md">
<Button onClick={()=>{
onClose()
action()
}} color="teal" type="submit">Yes</Button>
<Button onClick={onClose} color="red" type="submit">No</Button>
</Group>
</Modal>
}
export default YesNoModal;

View File

@@ -1,227 +1,227 @@
import { showNotification } from "@mantine/notifications";
import { ImCross } from "react-icons/im";
import { TiTick } from "react-icons/ti"
import { Navigate } from "react-router";
import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models";
import { Buffer } from "buffer"
import { QueryClient, useQuery } from "@tanstack/react-query";
import { useMediaQuery } from "@mantine/hooks";
import { io } from "socket.io-client";
import { useAuthStore, useSessionStore } from "./store";
export const IS_DEV = import.meta.env.DEV
export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$";
export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$";
export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$"
export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$"
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
export const DEV_IP_BACKEND = "127.0.0.1:4444"
export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
return value as E;
}
export const socketio = import.meta.env.DEV?
io("ws://"+DEV_IP_BACKEND, {
path:"/sock/socket.io",
transports: ['websocket'],
auth: {
token: useAuthStore.getState().getAccessToken()
}
}):
io({
path:"/sock/socket.io",
transports: ['websocket'],
auth: {
token: useAuthStore.getState().getAccessToken()
}
})
export const queryClient = new QueryClient({ defaultOptions: { queries: {
staleTime: Infinity
} }})
export function getErrorMessage(e: any) {
let error = "Unknown error";
if(typeof e == "string") return e
if (e.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
error = e.response.data.error;
} else {
// Something happened in setting up the request that triggered an Error
error = e.message || e.error;
}
return error;
}
export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") {
if (e.status){
return e.status
}
if (e.detail){
if (typeof e.detail == "string")
return e.detail
if (e.detail[0] && e.detail[0].msg)
return e.detail[0].msg
}
if (e.error){
return e.error
}
return def
}
export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
return await new Promise((resolve, reject) => {
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
method: method,
credentials: "same-origin",
cache: 'no-cache',
headers: {
...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}),
"Authorization" : "Bearer " + useAuthStore.getState().getAccessToken()
},
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"})
if(!res.ok){
const errorDefault = res.statusText
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
}
res.text().then(t => {
try{
resolve(JSON.parse(t))
}catch(e){
resolve(t)
}
}).catch( err => reject(err))
}).catch(err => {
reject(err)
})
});
}
export async function getapi(path:string):Promise<any>{
return await genericapi("GET",path)
}
export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise<any>{
return await genericapi("POST",path,data,is_form)
}
export async function deleteapi(path:string):Promise<any>{
return await genericapi("DELETE",path)
}
export async function putapi(path:string,data:any):Promise<any>{
return await genericapi("PUT",path,data)
}
export function getMainPath(){
const paths = window.location.pathname.split("/")
if (paths.length > 1) return paths[1]
return ""
}
export function HomeRedirector(){
const section = useSessionStore.getState().getHomeSection();
const path = section?`/${section}`:`/nfregex`
return <Navigate to={path} replace/>
}
export async function resetfiregex(delete_data:boolean = false){
const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse;
return (status === "ok"?undefined:status)
}
export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces)
export async function getipinterfaces(){
return await getapi("interfaces") as IpInterface[];
}
export async function getstatus(){
return await getapi(`status`) as ServerStatusResponse;
}
export async function logout(){
useAuthStore.getState().clearAccessToken();
}
export async function setpassword(data:PasswordSend) {
const { status, access_token } = await postapi("set-password",data) as ServerResponseToken;
if (access_token)
useAuthStore.getState().setAccessToken(access_token);
return status === "ok"?undefined:status
}
export async function changepassword(data:ChangePassword) {
const { status, access_token } = await postapi("change-password",data) as ServerResponseToken;
if (access_token)
useAuthStore.getState().setAccessToken(access_token);
return status === "ok"?undefined:status
}
export async function login(data:PasswordSend) {
const from = {username: "login", password: data.password};
const { status, access_token } = await postapi("login",from,true) as LoginResponse;
useAuthStore.getState().setAccessToken(access_token);
return status;
}
export function errorNotify(title:string, description:string ){
showNotification({
autoClose: 2000,
title: title,
message: description,
color: 'red',
icon: <ImCross />,
});
}
export function okNotify(title:string, description:string ){
showNotification({
autoClose: 2000,
title: title,
message: description,
color: 'teal',
icon: <TiTick />,
});
}
export const makeid = (length:number) => {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result;
}
export function b64encode(data:number[]|string){
return Buffer.from(data).toString('base64')
}
export function b64decode(regexB64:string){
return Buffer.from(regexB64, "base64").toString()
}
export function isMediumScreen(){
return useMediaQuery('(min-width: 600px)');
}
export function isLargeScreen(){
return useMediaQuery('(min-width: 992px)');
import { showNotification } from "@mantine/notifications";
import { ImCross } from "react-icons/im";
import { TiTick } from "react-icons/ti"
import { Navigate } from "react-router";
import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models";
import { Buffer } from "buffer"
import { QueryClient, useQuery } from "@tanstack/react-query";
import { useMediaQuery } from "@mantine/hooks";
import { io } from "socket.io-client";
import { useAuthStore, useSessionStore } from "./store";
export const IS_DEV = import.meta.env.DEV
export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$";
export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$";
export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$"
export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$"
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
export const DEV_IP_BACKEND = "127.0.0.1:4444"
export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
return value as E;
}
export const socketio = import.meta.env.DEV?
io("ws://"+DEV_IP_BACKEND, {
path:"/sock/socket.io",
transports: ['websocket'],
auth: {
token: useAuthStore.getState().getAccessToken()
}
}):
io({
path:"/sock/socket.io",
transports: ['websocket'],
auth: {
token: useAuthStore.getState().getAccessToken()
}
})
export const queryClient = new QueryClient({ defaultOptions: { queries: {
staleTime: Infinity
} }})
export function getErrorMessage(e: any) {
let error = "Unknown error";
if(typeof e == "string") return e
if (e.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
error = e.response.data.error;
} else {
// Something happened in setting up the request that triggered an Error
error = e.message || e.error;
}
return error;
}
export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") {
if (e.status){
return e.status
}
if (e.detail){
if (typeof e.detail == "string")
return e.detail
if (e.detail[0] && e.detail[0].msg)
return e.detail[0].msg
}
if (e.error){
return e.error
}
return def
}
export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
return await new Promise((resolve, reject) => {
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
method: method,
credentials: "same-origin",
cache: 'no-cache',
headers: {
...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}),
"Authorization" : "Bearer " + useAuthStore.getState().getAccessToken()
},
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"})
if(!res.ok){
const errorDefault = res.statusText
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
}
res.text().then(t => {
try{
resolve(JSON.parse(t))
}catch(e){
resolve(t)
}
}).catch( err => reject(err))
}).catch(err => {
reject(err)
})
});
}
export async function getapi(path:string):Promise<any>{
return await genericapi("GET",path)
}
export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise<any>{
return await genericapi("POST",path,data,is_form)
}
export async function deleteapi(path:string):Promise<any>{
return await genericapi("DELETE",path)
}
export async function putapi(path:string,data:any):Promise<any>{
return await genericapi("PUT",path,data)
}
export function getMainPath(){
const paths = window.location.pathname.split("/")
if (paths.length > 1) return paths[1]
return ""
}
export function HomeRedirector(){
const section = useSessionStore.getState().getHomeSection();
const path = section?`/${section}`:`/nfregex`
return <Navigate to={path} replace/>
}
export async function resetfiregex(delete_data:boolean = false){
const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse;
return (status === "ok"?undefined:status)
}
export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces)
export async function getipinterfaces(){
return await getapi("interfaces") as IpInterface[];
}
export async function getstatus(){
return await getapi(`status`) as ServerStatusResponse;
}
export async function logout(){
useAuthStore.getState().clearAccessToken();
}
export async function setpassword(data:PasswordSend) {
const { status, access_token } = await postapi("set-password",data) as ServerResponseToken;
if (access_token)
useAuthStore.getState().setAccessToken(access_token);
return status === "ok"?undefined:status
}
export async function changepassword(data:ChangePassword) {
const { status, access_token } = await postapi("change-password",data) as ServerResponseToken;
if (access_token)
useAuthStore.getState().setAccessToken(access_token);
return status === "ok"?undefined:status
}
export async function login(data:PasswordSend) {
const from = {username: "login", password: data.password};
const { status, access_token } = await postapi("login",from,true) as LoginResponse;
useAuthStore.getState().setAccessToken(access_token);
return status;
}
export function errorNotify(title:string, description:string ){
showNotification({
autoClose: 2000,
title: title,
message: description,
color: 'red',
icon: <ImCross />,
});
}
export function okNotify(title:string, description:string ){
showNotification({
autoClose: 2000,
title: title,
message: description,
color: 'teal',
icon: <TiTick />,
});
}
export const makeid = (length:number) => {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result;
}
export function b64encode(data:number[]|string){
return Buffer.from(data).toString('base64')
}
export function b64decode(regexB64:string){
return Buffer.from(regexB64, "base64").toString()
}
export function isMediumScreen(){
return useMediaQuery('(min-width: 600px)');
}
export function isLargeScreen(){
return useMediaQuery('(min-width: 992px)');
}

View File

@@ -1,237 +1,243 @@
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
import { Navigate, useNavigate, useParams } from 'react-router';
import { Badge, Divider, Menu } from '@mantine/core';
import { useEffect, useState } from 'react';
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
import { MdDoubleArrow } from "react-icons/md"
import YesNoModal from '../../components/YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
import { MenuDropDownWithButton } from '../../components/MainLayout';
import { useQueryClient } from '@tanstack/react-query';
import { FaArrowLeft } from "react-icons/fa";
import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../../components/NFProxy/AddEditService';
import PyFilterView from '../../components/PyFilterView';
import { TbPlugConnected } from 'react-icons/tb';
import { CodeHighlight } from '@mantine/code-highlight';
import { FaPython } from "react-icons/fa";
import { FiFileText } from "react-icons/fi";
import { ModalLog } from '../../components/ModalLog';
import { useListState } from '@mantine/hooks';
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
import { DocsButton } from '../../components/DocsButton';
export default function ServiceDetailsNFProxy() {
const {srv} = useParams()
const services = nfproxyServiceQuery()
const serviceInfo = services.data?.find(s => s.service_id == srv)
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const [buttonLoading, setButtonLoading] = useState(false)
const queryClient = useQueryClient()
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
const navigate = useNavigate()
const isMedium = isMediumScreen()
const [openLogModal, setOpenLogModal] = useState(false)
const [logData, logDataSetters] = useListState<string>([]);
useEffect(()=>{
if (srv){
if (openLogModal){
logDataSetters.setState([])
socketio.emit("nfproxy-outstream-join", { service: srv });
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
logDataSetters.append(data)
});
}else{
socketio.emit("nfproxy-outstream-leave", { service: srv });
socketio.off(`nfproxy-outstream-${srv}`);
logDataSetters.setState([])
}
return () => {
socketio.emit("nfproxy-outstream-leave", { service: srv });
socketio.off(`nfproxy-outstream-${srv}`);
logDataSetters.setState([])
}
}
}, [openLogModal, srv])
if (services.isLoading) return <LoadingOverlay visible={true} />
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
let status_color = "gray";
switch(serviceInfo.status){
case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break;
}
const startService = async () => {
setButtonLoading(true)
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false)
}
const deleteService = () => {
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
if (!res){
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey)
}else
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
})
}
const stopService = async () => {
setButtonLoading(true)
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false);
}
return <>
<LoadingOverlay visible={filtersList.isLoading} />
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box>
<Title order={1}>
<Box className="center-flex">
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
</Box>
</Title>
</Box>
{isMedium?null:<Space h="md" />}
<Box className='center-flex'>
<ExceptionWarning service_id={srv} />
<Space w="sm" />
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
{serviceInfo.status}
</Badge>
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
:{serviceInfo.port}
</Badge>
<MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
<Space w="md"/>
<Tooltip label="Show logs" zIndex={0} color="cyan">
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
<FiFileText size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
{isMedium?null:<Space h="md" />}
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Box className='center-flex'>
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
<Space w="xs" />
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
</Box>
{isMedium?<Space w="xs" />:<Space h="xs" />}
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
</Box>
{isMedium?null:<Space h="xl" />}
<Box className='center-flex'>
<Tooltip label="Go back" zIndex={0} color="cyan">
<ActionIcon color="cyan"
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
aria-describedby="tooltip-back-id">
<FaArrowLeft size="25px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={serviceInfo.status === "stop"}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
<Divider my="xl" />
{filterCode.data?<>
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
</>: null}
{(!filtersList.data || filtersList.data.length == 0)?<>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
}
<YesNoModal
title='Are you sure to delete this service?'
description={`You are going to delete the service '${serviceInfo.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={serviceInfo}
/>
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={serviceInfo}
/>
<ModalLog
opened={openLogModal}
close={()=>setOpenLogModal(false)}
title={`Logs for service ${serviceInfo.name}`}
data={logData.join("")}
/>
</>
}
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
import { Navigate, useNavigate, useParams } from 'react-router';
import { Badge, Divider, Menu } from '@mantine/core';
import { useEffect, useState } from 'react';
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
import { MdDoubleArrow } from "react-icons/md"
import YesNoModal from '../../components/YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
import { MenuDropDownWithButton } from '../../components/MainLayout';
import { useQueryClient } from '@tanstack/react-query';
import { FaArrowLeft } from "react-icons/fa";
import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../../components/NFProxy/AddEditService';
import PyFilterView from '../../components/PyFilterView';
import { TbPlugConnected } from 'react-icons/tb';
import { CodeHighlight } from '@mantine/code-highlight';
import { FaPython } from "react-icons/fa";
import { FiFileText } from "react-icons/fi";
import { ModalLog } from '../../components/ModalLog';
import { useListState } from '@mantine/hooks';
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
import { DocsButton } from '../../components/DocsButton';
export default function ServiceDetailsNFProxy() {
const {srv} = useParams()
const services = nfproxyServiceQuery()
const serviceInfo = services.data?.find(s => s.service_id == srv)
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const [buttonLoading, setButtonLoading] = useState(false)
const queryClient = useQueryClient()
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
const navigate = useNavigate()
const isMedium = isMediumScreen()
const [openLogModal, setOpenLogModal] = useState(false)
const [logData, logDataSetters] = useListState<string>([]);
useEffect(()=>{
if (srv){
if (openLogModal){
logDataSetters.setState([])
socketio.emit("nfproxy-outstream-join", { service: srv });
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
logDataSetters.append(data)
});
}else{
socketio.emit("nfproxy-outstream-leave", { service: srv });
socketio.off(`nfproxy-outstream-${srv}`);
logDataSetters.setState([])
}
return () => {
socketio.emit("nfproxy-outstream-leave", { service: srv });
socketio.off(`nfproxy-outstream-${srv}`);
logDataSetters.setState([])
}
}
}, [openLogModal, srv])
if (services.isLoading) return <LoadingOverlay visible={true} />
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
let status_color = "gray";
switch(serviceInfo.status){
case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break;
}
const startService = async () => {
setButtonLoading(true)
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false)
}
const deleteService = () => {
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
if (!res){
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey)
}else
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
})
}
const stopService = async () => {
setButtonLoading(true)
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false);
}
return <>
<LoadingOverlay visible={filtersList.isLoading} />
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box>
<Title order={1}>
<Box className="center-flex">
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
</Box>
</Title>
</Box>
{isMedium?null:<Space h="md" />}
<Box className='center-flex'>
<ExceptionWarning service_id={srv} />
<Space w="sm" />
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
{serviceInfo.status}
</Badge>
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
:{serviceInfo.port}
</Badge>
<MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
<Space w="md"/>
<Tooltip label="Show logs" zIndex={0} color="cyan">
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
<FiFileText size="20px" />
</ActionIcon>
</Tooltip>
<Space w="xs"/>
<Tooltip label="Traffic viewer" zIndex={0} color="grape">
<ActionIcon color="grape" size="lg" radius="md" onClick={()=>navigate(`/nfproxy/${srv}/traffic`)} variant="filled">
<MdDoubleArrow size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
{isMedium?null:<Space h="md" />}
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Box className='center-flex'>
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
<Space w="xs" />
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
</Box>
{isMedium?<Space w="xs" />:<Space h="xs" />}
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
</Box>
{isMedium?null:<Space h="xl" />}
<Box className='center-flex'>
<Tooltip label="Go back" zIndex={0} color="cyan">
<ActionIcon color="cyan"
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
aria-describedby="tooltip-back-id">
<FaArrowLeft size="25px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={serviceInfo.status === "stop"}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
<Divider my="xl" />
{filterCode.data?<>
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
</>: null}
{(!filtersList.data || filtersList.data.length == 0)?<>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
}
<YesNoModal
title='Are you sure to delete this service?'
description={`You are going to delete the service '${serviceInfo.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={serviceInfo}
/>
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={serviceInfo}
/>
<ModalLog
opened={openLogModal}
close={()=>setOpenLogModal(false)}
title={`Logs for service ${serviceInfo.name}`}
data={logData.join("")}
/>
</>
}

View File

@@ -0,0 +1,239 @@
import { ActionIcon, Badge, Box, Code, Divider, Grid, LoadingOverlay, Modal, ScrollArea, Space, Table, Text, TextInput, Title, Tooltip } from '@mantine/core';
import { Navigate, useNavigate, useParams } from 'react-router';
import { useEffect, useState } from 'react';
import { nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
import { errorNotify, isMediumScreen, socketio } from '../../js/utils';
import { FaArrowLeft, FaFilter, FaTrash } from 'react-icons/fa';
import { MdDoubleArrow } from "react-icons/md";
import { useListState } from '@mantine/hooks';
type TrafficEvent = {
ts: number;
direction?: string;
src_ip?: string;
src_port?: number;
dst_ip?: string;
dst_port?: number;
proto?: string;
size?: number;
verdict?: string;
filter?: string;
sample_hex?: string;
};
export default function TrafficViewer() {
const { srv } = useParams();
const services = nfproxyServiceQuery();
const serviceInfo = services.data?.find((s: any) => s.service_id === srv);
const navigate = useNavigate();
const isMedium = isMediumScreen();
const [events, eventsHandlers] = useListState<TrafficEvent>([]);
const [loading, setLoading] = useState(true);
const [filterText, setFilterText] = useState('');
const [selectedEvent, setSelectedEvent] = useState<TrafficEvent | null>(null);
const [modalOpened, setModalOpened] = useState(false);
useEffect(() => {
if (!srv) return;
// Fetch historical events
const fetchHistory = async () => {
try {
const response = await nfproxy.gettraffic(srv, 500);
if (response.events) {
eventsHandlers.setState(response.events);
}
} catch (err) {
console.error('Failed to fetch traffic history:', err);
} finally {
setLoading(false);
}
};
fetchHistory();
// Join Socket.IO room
socketio.emit("nfproxy-traffic-join", { service: srv });
// Listen for historical events on initial join
socketio.on("nfproxy-traffic-history", (data: { events: TrafficEvent[] }) => {
if (data.events && data.events.length > 0) {
eventsHandlers.setState(data.events);
}
});
// Listen for live events
socketio.on(`nfproxy-traffic-${srv}`, (event: TrafficEvent) => {
eventsHandlers.append(event);
});
return () => {
socketio.emit("nfproxy-traffic-leave", { service: srv });
socketio.off(`nfproxy-traffic-${srv}`);
socketio.off("nfproxy-traffic-history");
};
}, [srv]);
if (services.isLoading) return <LoadingOverlay visible={true} />;
if (!srv || !serviceInfo) return <Navigate to="/" replace />;
const clearEvents = async () => {
try {
await nfproxy.cleartraffic(srv);
eventsHandlers.setState([]);
} catch (err) {
errorNotify("Failed to clear traffic events", String(err));
}
};
const filteredEvents = events.filter((e: TrafficEvent) => {
if (!filterText) return true;
const search = filterText.toLowerCase();
return (
e.src_ip?.toLowerCase().includes(search) ||
e.dst_ip?.toLowerCase().includes(search) ||
e.verdict?.toLowerCase().includes(search) ||
e.filter?.toLowerCase().includes(search) ||
e.proto?.toLowerCase().includes(search)
);
});
const formatTimestamp = (ts: number) => {
const date = new Date(ts);
return date.toLocaleTimeString() + '.' + date.getMilliseconds().toString().padStart(3, '0');
};
const getVerdictColor = (verdict?: string) => {
switch (verdict?.toLowerCase()) {
case 'accept': return 'teal';
case 'drop': return 'red';
case 'reject': return 'orange';
case 'edited': return 'yellow';
default: return 'gray';
}
};
const openDetails = (event: TrafficEvent) => {
setSelectedEvent(event);
setModalOpened(true);
};
return <>
<LoadingOverlay visible={loading} />
<Box className={isMedium ? 'center-flex' : 'center-flex-row'} style={{ justifyContent: "space-between" }} px="md" mt="lg">
<Title order={1}>
<Box className="center-flex">
<MdDoubleArrow /><Space w="sm" />Traffic Viewer - {serviceInfo.name}
</Box>
</Title>
<Box className='center-flex'>
<Badge color="cyan" radius="md" size="xl" variant="filled" mr="sm">
{filteredEvents.length} events
</Badge>
<Tooltip label="Clear events" color="red">
<ActionIcon color="red" size="lg" radius="md" onClick={clearEvents} variant="filled">
<FaTrash size="18px" />
</ActionIcon>
</Tooltip>
<Space w="md" />
<Tooltip label="Go back" color="cyan">
<ActionIcon color="cyan" onClick={() => navigate(-1)} size="lg" radius="md" variant="filled">
<FaArrowLeft size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
<Divider my="md" />
<Box px="md">
<TextInput
placeholder="Filter by IP, verdict, filter name, or protocol..."
value={filterText}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
leftSection={<FaFilter />}
mb="md"
/>
<ScrollArea style={{ height: 'calc(100vh - 280px)' }}>
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Time</Table.Th>
<Table.Th>Direction</Table.Th>
<Table.Th>Source</Table.Th>
<Table.Th>Destination</Table.Th>
<Table.Th>Protocol</Table.Th>
<Table.Th>Size</Table.Th>
<Table.Th>Filter</Table.Th>
<Table.Th>Verdict</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{filteredEvents.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={8} style={{ textAlign: 'center', padding: '2rem' }}>
<Text c="dimmed">
{filterText ? 'No events match your filter' : 'No traffic events yet. Waiting for traffic...'}
</Text>
</Table.Td>
</Table.Tr>
) : (
filteredEvents.slice(-500).reverse().map((event: TrafficEvent, idx: number) => (
<Table.Tr key={idx} onClick={() => openDetails(event)} style={{ cursor: 'pointer' }}>
<Table.Td><Code>{formatTimestamp(event.ts)}</Code></Table.Td>
<Table.Td>
<Badge size="sm" variant="dot" color={event.direction === 'in' ? 'blue' : 'grape'}>
{event.direction || 'unknown'}
</Badge>
</Table.Td>
<Table.Td>{event.src_ip || '-'}:{event.src_port || '-'}</Table.Td>
<Table.Td>{event.dst_ip || '-'}:{event.dst_port || '-'}</Table.Td>
<Table.Td><Badge size="sm" color="violet">{event.proto || 'unknown'}</Badge></Table.Td>
<Table.Td>{event.size ? `${event.size} B` : '-'}</Table.Td>
<Table.Td><Code>{event.filter || '-'}</Code></Table.Td>
<Table.Td>
<Badge color={getVerdictColor(event.verdict)} size="sm">
{event.verdict || 'unknown'}
</Badge>
</Table.Td>
</Table.Tr>
))
)}
</Table.Tbody>
</Table>
</ScrollArea>
</Box>
{/* Payload details modal */}
<Modal
opened={modalOpened}
onClose={() => setModalOpened(false)}
title="Event Details"
size="xl"
>
{selectedEvent && (
<Box>
<Grid>
<Grid.Col span={6}><strong>Timestamp:</strong> {formatTimestamp(selectedEvent.ts)}</Grid.Col>
<Grid.Col span={6}><strong>Direction:</strong> {selectedEvent.direction || 'unknown'}</Grid.Col>
<Grid.Col span={6}><strong>Source:</strong> {selectedEvent.src_ip}:{selectedEvent.src_port}</Grid.Col>
<Grid.Col span={6}><strong>Destination:</strong> {selectedEvent.dst_ip}:{selectedEvent.dst_port}</Grid.Col>
<Grid.Col span={6}><strong>Protocol:</strong> {selectedEvent.proto || 'unknown'}</Grid.Col>
<Grid.Col span={6}><strong>Size:</strong> {selectedEvent.size ? `${selectedEvent.size} B` : '-'}</Grid.Col>
<Grid.Col span={6}><strong>Filter:</strong> {selectedEvent.filter || '-'}</Grid.Col>
<Grid.Col span={6}><strong>Verdict:</strong> {selectedEvent.verdict || 'unknown'}</Grid.Col>
</Grid>
{selectedEvent.sample_hex && (
<>
<Divider my="md" label="Payload Sample (Hex)" />
<ScrollArea style={{ maxHeight: '300px' }}>
<Code block>{selectedEvent.sample_hex}</Code>
</ScrollArea>
</>
)}
</Box>
)}
</Modal>
</>;
}

View File

@@ -1,172 +1,172 @@
import { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react';
import { BsPlusLg } from "react-icons/bs";
import { useNavigate, useParams } from 'react-router';
import ServiceRow from '../../components/NFProxy/ServiceRow';
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
import AddEditService from '../../components/NFProxy/AddEditService';
import { useQueryClient } from '@tanstack/react-query';
import { TbPlugConnected, TbReload } from 'react-icons/tb';
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
import { MdUploadFile } from "react-icons/md";
import { notifications } from '@mantine/notifications';
import { useFileDialog } from '@mantine/hooks';
import { CodeHighlight } from '@mantine/code-highlight';
import { DocsButton } from '../../components/DocsButton';
export default function NFProxy({ children }: { children: any }) {
const navigator = useNavigate()
const [open, setOpen] = useState(false);
const {srv} = useParams()
const queryClient = useQueryClient()
const isMedium = isMediumScreen()
const services = nfproxyServiceQuery()
const fileDialog = useFileDialog({
accept: ".py",
multiple: false,
resetOnOpen: true,
onChange: (files) => {
if (files?.length??0 > 0)
setFile(files![0])
}
});
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
if (!srv) return
const service = services.data?.find(s => s.service_id === srv)
if (!service) return
if (file){
console.log("Uploading code")
const notify_id = notifications.show(
{
title: "Uploading code",
message: `Uploading code for service ${service.name}`,
color: "blue",
icon: <MdUploadFile size={20} />,
autoClose: false,
loading: true,
}
)
file.text()
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
.then( res => {
if (!res){
notifications.update({
id: notify_id,
title: "Code uploaded",
message: `Successfully uploaded code for service ${service.name}`,
color: "green",
icon: <MdUploadFile size={20} />,
autoClose: 5000,
loading: false,
})
}else{
notifications.update({
id: notify_id,
title: "Code upload failed",
message: `Error: ${res}`,
color: "red",
icon: <MdUploadFile size={20} />,
autoClose: 5000,
loading: false,
})
}
}).catch( err => {
notifications.update({
id: notify_id,
title: "Code upload failed",
message: `Error: ${err}`,
color: "red",
icon: <MdUploadFile size={20} />,
autoClose: 5000,
loading: false,
})
}).finally(()=>{setFile(null)})
}
}, [file])
useEffect(()=> {
if(services.isError)
errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
},[services.isError])
const closeModal = () => {setOpen(false);}
return <>
<Space h="sm" />
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex' >
{isMedium?"General stats:":null}
<Space w="xs" />
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
<Space w="xs" />
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
<Space w="xs" />
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
<Space w="xs" />
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
<Space w="xs" />
</Box>
{isMedium?null:<Space h="md" />}
<Box className='center-flex' >
{ srv?
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
<MdUploadFile size={18} />
</ActionIcon>
</Tooltip>
: <Tooltip label="Add a new service" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
<BsPlusLg size={18} />
</ActionIcon>
</Tooltip>
}
<Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
<TbReload size={18} />
</ActionIcon>
</Tooltip>
<Space w="xs" />
<DocsButton doc="nfproxy" />
</Box>
</Box>
<Space h="md" />
<Box className="center-flex-row" style={{gap: 20}}>
{srv?null:<>
<LoadingOverlay visible={services.isLoading} />
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
navigator("/nfproxy/"+srv.service_id)
}} />):<>
<Box className='center-flex-row'>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter proxy is a simulated proxy written using python with a c++ core</Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Then you can create a new service and write custom filters for the service</Title>
<Space h="lg" />
<Box className='center-flex' style={{gap: 20}}>
<Tooltip label="Add a new service" color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
<BsPlusLg size="20px" />
</ActionIcon>
</Tooltip>
<DocsButton doc="nfproxy" size="xl" />
</Box>
</Box>
</>}
</>}
</Box>
{srv?children:null}
{!srv?
<AddEditService opened={open} onClose={closeModal} />:null
}
</>
}
import { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react';
import { BsPlusLg } from "react-icons/bs";
import { useNavigate, useParams } from 'react-router';
import ServiceRow from '../../components/NFProxy/ServiceRow';
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
import AddEditService from '../../components/NFProxy/AddEditService';
import { useQueryClient } from '@tanstack/react-query';
import { TbPlugConnected, TbReload } from 'react-icons/tb';
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
import { MdUploadFile } from "react-icons/md";
import { notifications } from '@mantine/notifications';
import { useFileDialog } from '@mantine/hooks';
import { CodeHighlight } from '@mantine/code-highlight';
import { DocsButton } from '../../components/DocsButton';
export default function NFProxy({ children }: { children: any }) {
const navigator = useNavigate()
const [open, setOpen] = useState(false);
const {srv} = useParams()
const queryClient = useQueryClient()
const isMedium = isMediumScreen()
const services = nfproxyServiceQuery()
const fileDialog = useFileDialog({
accept: ".py",
multiple: false,
resetOnOpen: true,
onChange: (files) => {
if (files?.length??0 > 0)
setFile(files![0])
}
});
const [file, setFile] = useState<File | null>(null);
useEffect(() => {
if (!srv) return
const service = services.data?.find(s => s.service_id === srv)
if (!service) return
if (file){
console.log("Uploading code")
const notify_id = notifications.show(
{
title: "Uploading code",
message: `Uploading code for service ${service.name}`,
color: "blue",
icon: <MdUploadFile size={20} />,
autoClose: false,
loading: true,
}
)
file.text()
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
.then( res => {
if (!res){
notifications.update({
id: notify_id,
title: "Code uploaded",
message: `Successfully uploaded code for service ${service.name}`,
color: "green",
icon: <MdUploadFile size={20} />,
autoClose: 5000,
loading: false,
})
}else{
notifications.update({
id: notify_id,
title: "Code upload failed",
message: `Error: ${res}`,
color: "red",
icon: <MdUploadFile size={20} />,
autoClose: 5000,
loading: false,
})
}
}).catch( err => {
notifications.update({
id: notify_id,
title: "Code upload failed",
message: `Error: ${err}`,
color: "red",
icon: <MdUploadFile size={20} />,
autoClose: 5000,
loading: false,
})
}).finally(()=>{setFile(null)})
}
}, [file])
useEffect(()=> {
if(services.isError)
errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
},[services.isError])
const closeModal = () => {setOpen(false);}
return <>
<Space h="sm" />
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex' >
{isMedium?"General stats:":null}
<Space w="xs" />
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
<Space w="xs" />
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
<Space w="xs" />
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
<Space w="xs" />
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
<Space w="xs" />
</Box>
{isMedium?null:<Space h="md" />}
<Box className='center-flex' >
{ srv?
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
<MdUploadFile size={18} />
</ActionIcon>
</Tooltip>
: <Tooltip label="Add a new service" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
<BsPlusLg size={18} />
</ActionIcon>
</Tooltip>
}
<Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
<TbReload size={18} />
</ActionIcon>
</Tooltip>
<Space w="xs" />
<DocsButton doc="nfproxy" />
</Box>
</Box>
<Space h="md" />
<Box className="center-flex-row" style={{gap: 20}}>
{srv?null:<>
<LoadingOverlay visible={services.isLoading} />
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
navigator("/nfproxy/"+srv.service_id)
}} />):<>
<Box className='center-flex-row'>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter proxy is a simulated proxy written using python with a c++ core</Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Then you can create a new service and write custom filters for the service</Title>
<Space h="lg" />
<Box className='center-flex' style={{gap: 20}}>
<Tooltip label="Add a new service" color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
<BsPlusLg size="20px" />
</ActionIcon>
</Tooltip>
<DocsButton doc="nfproxy" size="xl" />
</Box>
</Box>
</>}
</>}
</Box>
{srv?children:null}
{!srv?
<AddEditService opened={open} onClose={closeModal} />:null
}
</>
}

View File

@@ -1,194 +1,194 @@
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
import { Navigate, useNavigate, useParams } from 'react-router';
import RegexView from '../../components/RegexView';
import AddNewRegex from '../../components/AddNewRegex';
import { BsPlusLg } from "react-icons/bs";
import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils';
import { Badge, Divider, Menu } from '@mantine/core';
import { useState } from 'react';
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
import { MdDoubleArrow } from "react-icons/md"
import YesNoModal from '../../components/YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
import { MenuDropDownWithButton } from '../../components/MainLayout';
import { useQueryClient } from '@tanstack/react-query';
import { FaArrowLeft } from "react-icons/fa";
import { VscRegex } from 'react-icons/vsc';
import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../../components/NFRegex/AddEditService';
export default function ServiceDetailsNFRegex() {
const {srv} = useParams()
const [open, setOpen] = useState(false)
const services = nfregexServiceQuery()
const serviceInfo = services.data?.find(s => s.service_id == srv)
const regexesList = nfregexServiceRegexesQuery(srv??"")
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const [buttonLoading, setButtonLoading] = useState(false)
const queryClient = useQueryClient()
const navigate = useNavigate()
const isMedium = isMediumScreen()
if (services.isLoading) return <LoadingOverlay visible={true} />
if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace />
let status_color = "gray";
switch(serviceInfo.status){
case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break;
}
const startService = async () => {
setButtonLoading(true)
await nfregex.servicestart(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false)
}
const deleteService = () => {
nfregex.servicedelete(serviceInfo.service_id).then(res => {
if (!res){
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey)
}else
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
})
}
const stopService = async () => {
setButtonLoading(true)
await nfregex.servicestop(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false);
}
return <>
<LoadingOverlay visible={regexesList.isLoading} />
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box>
<Title order={1}>
<Box className="center-flex">
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
</Box>
</Title>
</Box>
{isMedium?null:<Space h="md" />}
<Box className='center-flex'>
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
{serviceInfo.status}
</Badge>
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
:{serviceInfo.port}
</Badge>
<MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
</Box>
</Box>
{isMedium?null:<Space h="md" />}
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Box className='center-flex'>
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
</Box>
{isMedium?<Space w="xs" />:<Space h="xs" />}
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
</Box>
{isMedium?null:<Space h="xl" />}
<Box className='center-flex'>
<Tooltip label="Go back" zIndex={0} color="cyan">
<ActionIcon color="cyan"
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
aria-describedby="tooltip-back-id">
<FaArrowLeft size="25px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={serviceInfo.status === "stop"}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
<Divider my="xl" />
{(!regexesList.data || regexesList.data.length == 0)?<>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
<Space h="xl" /> <Space h="xl" />
<Box className='center-flex'>
<Tooltip label="Add a new regex" zIndex={0} color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
</Tooltip>
</Box>
</>:
<Grid>
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
</Grid>
}
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
<YesNoModal
title='Are you sure to delete this service?'
description={`You are going to delete the service '${serviceInfo.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={serviceInfo}
/>
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={serviceInfo}
/>
</>
}
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
import { Navigate, useNavigate, useParams } from 'react-router';
import RegexView from '../../components/RegexView';
import AddNewRegex from '../../components/AddNewRegex';
import { BsPlusLg } from "react-icons/bs";
import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils';
import { Badge, Divider, Menu } from '@mantine/core';
import { useState } from 'react';
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
import { MdDoubleArrow } from "react-icons/md"
import YesNoModal from '../../components/YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi'
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
import { MenuDropDownWithButton } from '../../components/MainLayout';
import { useQueryClient } from '@tanstack/react-query';
import { FaArrowLeft } from "react-icons/fa";
import { VscRegex } from 'react-icons/vsc';
import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../../components/NFRegex/AddEditService';
export default function ServiceDetailsNFRegex() {
const {srv} = useParams()
const [open, setOpen] = useState(false)
const services = nfregexServiceQuery()
const serviceInfo = services.data?.find(s => s.service_id == srv)
const regexesList = nfregexServiceRegexesQuery(srv??"")
const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false)
const [buttonLoading, setButtonLoading] = useState(false)
const queryClient = useQueryClient()
const navigate = useNavigate()
const isMedium = isMediumScreen()
if (services.isLoading) return <LoadingOverlay visible={true} />
if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace />
let status_color = "gray";
switch(serviceInfo.status){
case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break;
}
const startService = async () => {
setButtonLoading(true)
await nfregex.servicestart(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false)
}
const deleteService = () => {
nfregex.servicedelete(serviceInfo.service_id).then(res => {
if (!res){
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey)
}else
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
})
}
const stopService = async () => {
setButtonLoading(true)
await nfregex.servicestop(serviceInfo.service_id).then(res => {
if(!res){
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey)
}else{
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
}
}).catch(err => {
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
})
setButtonLoading(false);
}
return <>
<LoadingOverlay visible={regexesList.isLoading} />
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box>
<Title order={1}>
<Box className="center-flex">
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
</Box>
</Title>
</Box>
{isMedium?null:<Space h="md" />}
<Box className='center-flex'>
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
{serviceInfo.status}
</Badge>
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
:{serviceInfo.port}
</Badge>
<MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider />
<Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton>
</Box>
</Box>
{isMedium?null:<Space h="md" />}
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Box className='center-flex'>
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
<Space w="xs" />
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
</Box>
{isMedium?<Space w="xs" />:<Space h="xs" />}
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
</Box>
{isMedium?null:<Space h="xl" />}
<Box className='center-flex'>
<Tooltip label="Go back" zIndex={0} color="cyan">
<ActionIcon color="cyan"
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
aria-describedby="tooltip-back-id">
<FaArrowLeft size="25px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Stop service" zIndex={0} color="red">
<ActionIcon color="red" loading={buttonLoading}
onClick={stopService} size="xl" radius="md" variant="filled"
disabled={serviceInfo.status === "stop"}
aria-describedby="tooltip-stop-id">
<FaStop size="20px" />
</ActionIcon>
</Tooltip>
<Space w="md"/>
<Tooltip label="Start service" zIndex={0} color="teal">
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
<FaPlay size="20px" />
</ActionIcon>
</Tooltip>
</Box>
</Box>
<Divider my="xl" />
{(!regexesList.data || regexesList.data.length == 0)?<>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
<Space h="xl" /> <Space h="xl" />
<Box className='center-flex'>
<Tooltip label="Add a new regex" zIndex={0} color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
</Tooltip>
</Box>
</>:
<Grid>
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
</Grid>
}
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
<YesNoModal
title='Are you sure to delete this service?'
description={`You are going to delete the service '${serviceInfo.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={serviceInfo}
/>
<AddEditService
opened={editModal}
onClose={()=>setEditModal(false)}
edit={serviceInfo}
/>
</>
}

View File

@@ -1,100 +1,100 @@
import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react';
import { BsPlusLg, BsRegex } from "react-icons/bs";
import { useNavigate, useParams } from 'react-router';
import ServiceRow from '../../components/NFRegex/ServiceRow';
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
import AddEditService from '../../components/NFRegex/AddEditService';
import AddNewRegex from '../../components/AddNewRegex';
import { useQueryClient } from '@tanstack/react-query';
import { TbReload } from 'react-icons/tb';
import { FaFilter } from 'react-icons/fa';
import { FaServer } from "react-icons/fa6";
import { VscRegex } from "react-icons/vsc";
import { DocsButton } from '../../components/DocsButton';
function NFRegex({ children }: { children: any }) {
const navigator = useNavigate()
const [open, setOpen] = useState(false);
const {srv} = useParams()
const queryClient = useQueryClient()
const isMedium = isMediumScreen()
const services = nfregexServiceQuery()
useEffect(()=> {
if(services.isError)
errorNotify("NFRegex Update failed!", getErrorMessage(services.error))
},[services.isError])
const closeModal = () => {setOpen(false);}
return <>
<Space h="sm" />
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex' >
{isMedium?"General stats:":null}
<Space w="xs" />
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
<Space w="xs" />
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
<Space w="xs" />
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
<Space w="xs" />
</Box>
{isMedium?null:<Space h="md" />}
<Box className='center-flex' >
{ srv?
<Tooltip label="Add a new regex" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
</Tooltip>
: <Tooltip label="Add a new service" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
</Tooltip>
}
<Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled"
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
</Tooltip>
<Space w="xs" />
<DocsButton doc="nfregex" />
</Box>
</Box>
<Space h="md" />
<Box className="center-flex-row" style={{gap: 20}}>
{srv?null:<>
<LoadingOverlay visible={services.isLoading} />
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
navigator("/nfregex/"+srv.service_id)
}} />):<>
<Box className='center-flex-row'>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter Regex allows you to filter traffic using regexes</Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start a service, add your regexes and it's already done!</Title>
<Space h="lg" />
<Box className='center-flex' style={{gap: 20}}>
<Tooltip label="Add a new service" color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
<BsPlusLg size="20px" />
</ActionIcon>
</Tooltip>
<DocsButton doc="nfregex" size="xl" />
</Box>
</Box>
</>}
</>}
</Box>
{srv?children:null}
{srv?
<AddNewRegex opened={open} onClose={closeModal} service={srv} />:
<AddEditService opened={open} onClose={closeModal} />
}
</>
}
export default NFRegex;
import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react';
import { BsPlusLg, BsRegex } from "react-icons/bs";
import { useNavigate, useParams } from 'react-router';
import ServiceRow from '../../components/NFRegex/ServiceRow';
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
import AddEditService from '../../components/NFRegex/AddEditService';
import AddNewRegex from '../../components/AddNewRegex';
import { useQueryClient } from '@tanstack/react-query';
import { TbReload } from 'react-icons/tb';
import { FaFilter } from 'react-icons/fa';
import { FaServer } from "react-icons/fa6";
import { VscRegex } from "react-icons/vsc";
import { DocsButton } from '../../components/DocsButton';
function NFRegex({ children }: { children: any }) {
const navigator = useNavigate()
const [open, setOpen] = useState(false);
const {srv} = useParams()
const queryClient = useQueryClient()
const isMedium = isMediumScreen()
const services = nfregexServiceQuery()
useEffect(()=> {
if(services.isError)
errorNotify("NFRegex Update failed!", getErrorMessage(services.error))
},[services.isError])
const closeModal = () => {setOpen(false);}
return <>
<Space h="sm" />
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex' >
{isMedium?"General stats:":null}
<Space w="xs" />
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
<Space w="xs" />
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
<Space w="xs" />
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
<Space w="xs" />
</Box>
{isMedium?null:<Space h="md" />}
<Box className='center-flex' >
{ srv?
<Tooltip label="Add a new regex" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
</Tooltip>
: <Tooltip label="Add a new service" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
</Tooltip>
}
<Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled"
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
</Tooltip>
<Space w="xs" />
<DocsButton doc="nfregex" />
</Box>
</Box>
<Space h="md" />
<Box className="center-flex-row" style={{gap: 20}}>
{srv?null:<>
<LoadingOverlay visible={services.isLoading} />
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
navigator("/nfregex/"+srv.service_id)
}} />):<>
<Box className='center-flex-row'>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter Regex allows you to filter traffic using regexes</Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start a service, add your regexes and it's already done!</Title>
<Space h="lg" />
<Box className='center-flex' style={{gap: 20}}>
<Tooltip label="Add a new service" color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
<BsPlusLg size="20px" />
</ActionIcon>
</Tooltip>
<DocsButton doc="nfregex" size="xl" />
</Box>
</Box>
</>}
</>}
</Box>
{srv?children:null}
{srv?
<AddNewRegex opened={open} onClose={closeModal} service={srv} />:
<AddEditService opened={open} onClose={closeModal} />
}
</>
}
export default NFRegex;

View File

@@ -1,77 +1,77 @@
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react';
import { BsPlusLg } from "react-icons/bs";
import ServiceRow from '../../components/PortHijack/ServiceRow';
import { porthijackServiceQuery } from '../../components/PortHijack/utils';
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
import AddNewService from '../../components/PortHijack/AddNewService';
import { useQueryClient } from '@tanstack/react-query';
import { TbReload } from 'react-icons/tb';
import { FaServer } from 'react-icons/fa';
import { GrDirections } from 'react-icons/gr';
import { DocsButton } from '../../components/DocsButton';
function PortHijack() {
const [open, setOpen] = useState(false);
const queryClient = useQueryClient()
const isMedium = isMediumScreen()
const services = porthijackServiceQuery()
useEffect(()=>{
if(services.isError)
errorNotify("Porthijack Update failed!", getErrorMessage(services.error))
},[services.isError])
const closeModal = () => {setOpen(false);}
return <>
<Space h="sm" />
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex'>
<Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
<Space w="xs" />
<Tooltip label="Add a new service" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
</Tooltip>
<Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
</Tooltip>
<Space w="xs" />
<DocsButton doc="porthijack" />
</Box>
</Box>
<Space h="md" />
<Box className="center-flex-row" style={{gap: 20}}>
<LoadingOverlay visible={services.isLoading} />
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
<Box className='center-flex-row'>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config</Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback</Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs</Title>
<Space h="lg" />
<Box className='center-flex' style={{gap: 20}}>
<Tooltip label="Add a new service" color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
<BsPlusLg size="20px" />
</ActionIcon>
</Tooltip>
<DocsButton doc="porthijack" size="xl" />
</Box>
</Box>
</>}
<AddNewService opened={open} onClose={closeModal} />
</Box>
</>
}
export default PortHijack;
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react';
import { BsPlusLg } from "react-icons/bs";
import ServiceRow from '../../components/PortHijack/ServiceRow';
import { porthijackServiceQuery } from '../../components/PortHijack/utils';
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
import AddNewService from '../../components/PortHijack/AddNewService';
import { useQueryClient } from '@tanstack/react-query';
import { TbReload } from 'react-icons/tb';
import { FaServer } from 'react-icons/fa';
import { GrDirections } from 'react-icons/gr';
import { DocsButton } from '../../components/DocsButton';
function PortHijack() {
const [open, setOpen] = useState(false);
const queryClient = useQueryClient()
const isMedium = isMediumScreen()
const services = porthijackServiceQuery()
useEffect(()=>{
if(services.isError)
errorNotify("Porthijack Update failed!", getErrorMessage(services.error))
},[services.isError])
const closeModal = () => {setOpen(false);}
return <>
<Space h="sm" />
<Box className={isMedium?'center-flex':'center-flex-row'}>
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex'>
<Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
<Space w="xs" />
<Tooltip label="Add a new service" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
</Tooltip>
<Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
</Tooltip>
<Space w="xs" />
<DocsButton doc="porthijack" />
</Box>
</Box>
<Space h="md" />
<Box className="center-flex-row" style={{gap: 20}}>
<LoadingOverlay visible={services.isLoading} />
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
<Box className='center-flex-row'>
<Space h="xl" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config</Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback</Title>
<Space h="xs" />
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs</Title>
<Space h="lg" />
<Box className='center-flex' style={{gap: 20}}>
<Tooltip label="Add a new service" color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
<BsPlusLg size="20px" />
</ActionIcon>
</Tooltip>
<DocsButton doc="porthijack" size="xl" />
</Box>
</Box>
</>}
<AddNewService opened={open} onClose={closeModal} />
</Box>
</>
}
export default PortHijack;

View File

@@ -0,0 +1,138 @@
import { ActionIcon, Badge, Box, Card, Divider, Group, LoadingOverlay, Space, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useNavigate } from 'react-router';
import { nfproxyServiceQuery } from '../../components/NFProxy/utils';
import { isMediumScreen } from '../../js/utils';
import { MdDoubleArrow, MdVisibility } from 'react-icons/md';
import { TbPlugConnected } from 'react-icons/tb';
import { FaServer } from 'react-icons/fa';
export default function TrafficViewer() {
const services = nfproxyServiceQuery();
const navigate = useNavigate();
const isMedium = isMediumScreen();
if (services.isLoading) return <LoadingOverlay visible={true} />;
const activeServices = services.data?.filter(s => s.status === 'active') || [];
const stoppedServices = services.data?.filter(s => s.status !== 'active') || [];
return <>
<Box px="md" mt="lg">
<Title order={1} className="center-flex">
<ThemeIcon radius="md" size="lg" variant='filled' color='cyan'>
<MdVisibility size={24} />
</ThemeIcon>
<Space w="sm" />
Traffic Viewer
</Title>
<Text c="dimmed" mt="sm">
Monitor live network traffic for all NFProxy services
</Text>
</Box>
<Divider my="lg" />
{services.data?.length === 0 ? (
<Box px="md">
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
No NFProxy services found
</Title>
<Space h="xs" />
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
Create a service in the Netfilter Proxy section to start monitoring traffic
</Text>
</Box>
) : (
<Box px="md">
{activeServices.length > 0 && (
<>
<Title order={3} mb="md">
<Badge color="teal" size="lg" mr="xs">Active</Badge>
Running Services
</Title>
{activeServices.map(service => (
<Card key={service.service_id} shadow="sm" padding="lg" radius="md" withBorder mb="md">
<Group justify="space-between">
<Box>
<Group>
<ThemeIcon color="lime" variant="light" size="lg">
<TbPlugConnected size={20} />
</ThemeIcon>
<div>
<Text fw={700} size="lg">{service.name}</Text>
<Group gap="xs" mt={4}>
<Badge color="cyan" size="sm">:{service.port}</Badge>
<Badge color="violet" size="sm">{service.proto}</Badge>
<Badge color="gray" size="sm">{service.ip_int}</Badge>
</Group>
</div>
</Group>
</Box>
<Box>
<Group>
<Box style={{ textAlign: 'right' }}>
<Badge color="orange" size="sm" mb={4}>
{service.edited_packets} edited
</Badge>
<br />
<Badge color="yellow" size="sm">
{service.blocked_packets} blocked
</Badge>
</Box>
<Tooltip label="View traffic">
<ActionIcon
color="cyan"
size="xl"
radius="md"
variant="filled"
onClick={() => navigate(`/nfproxy/${service.service_id}/traffic`)}
>
<MdDoubleArrow size="24px" />
</ActionIcon>
</Tooltip>
</Group>
</Box>
</Group>
</Card>
))}
<Space h="xl" />
</>
)}
{stoppedServices.length > 0 && (
<>
<Title order={3} mb="md">
<Badge color="red" size="lg" mr="xs">Stopped</Badge>
Inactive Services
</Title>
{stoppedServices.map(service => (
<Card key={service.service_id} shadow="sm" padding="lg" radius="md" withBorder mb="md" opacity={0.6}>
<Group justify="space-between">
<Box>
<Group>
<ThemeIcon color="gray" variant="light" size="lg">
<FaServer size={18} />
</ThemeIcon>
<div>
<Text fw={500} size="lg" c="dimmed">{service.name}</Text>
<Group gap="xs" mt={4}>
<Badge color="gray" size="sm">:{service.port}</Badge>
<Badge color="gray" size="sm">{service.proto}</Badge>
</Group>
</div>
</Group>
</Box>
<Box>
<Text size="sm" c="dimmed">
Start service to view traffic
</Text>
</Box>
</Group>
</Card>
))}
</>
)}
</Box>
)}
</>;
}

View File

@@ -1,22 +1,22 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
{
"compilerOptions": {
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}