diff --git a/backend/routers/nfregex.py b/backend/routers/nfregex.py index ef86980..45856d1 100644 --- a/backend/routers/nfregex.py +++ b/backend/routers/nfregex.py @@ -42,7 +42,7 @@ class RegexAddForm(BaseModel): service_id: str regex: str mode: str - active: bool|None + active: bool|None = None is_blacklist: bool is_case_sensitive: bool @@ -54,7 +54,7 @@ class ServiceAddForm(BaseModel): class ServiceAddResponse(BaseModel): status:str - service_id: str|None + service_id: str|None = None app = APIRouter() diff --git a/backend/routers/porthijack.py b/backend/routers/porthijack.py index 54c1314..18d1bd1 100644 --- a/backend/routers/porthijack.py +++ b/backend/routers/porthijack.py @@ -32,7 +32,7 @@ class ServiceAddForm(BaseModel): class ServiceAddResponse(BaseModel): status:str - service_id: str|None + service_id: str|None = None class GeneralStatModel(BaseModel): services: int diff --git a/backend/routers/regexproxy.py b/backend/routers/regexproxy.py index eec4834..ca60dc0 100644 --- a/backend/routers/regexproxy.py +++ b/backend/routers/regexproxy.py @@ -156,8 +156,8 @@ async def regen_service_port(service_id: str): return {'status': 'ok'} class ChangePortForm(BaseModel): - port: int|None - internalPort: int|None + port: int|None = None + internalPort: int|None = None @app.post('/service/{service_id}/change-ports', response_model=StatusMessageModel) async def change_service_ports(service_id: str, change_port:ChangePortForm): @@ -249,7 +249,7 @@ class RegexAddForm(BaseModel): service_id: str regex: str mode: str - active: bool|None + active: bool|None = None is_blacklist: bool is_case_sensitive: bool @@ -272,11 +272,11 @@ async def add_new_regex(form: RegexAddForm): class ServiceAddForm(BaseModel): name: str port: PortType - internalPort: int|None + internalPort: int|None = None class ServiceAddStatus(BaseModel): status:str - id: str|None + id: str|None = None class RenameForm(BaseModel): name:str diff --git a/backend/utils/loader.py b/backend/utils/loader.py index c37327a..3db07d0 100644 --- a/backend/utils/loader.py +++ b/backend/utils/loader.py @@ -48,10 +48,10 @@ def list_routers(): return [ele[:-3] for ele in list_files(ROUTERS_DIR) if ele != "__init__.py" and " " not in ele and ele.endswith(".py")] class RouterModule(): - router: None|APIRouter - reset: None|Callable - startup: None|Callable - shutdown: None|Callable + router: APIRouter|None = None + reset: Callable|None = None + startup: Callable|None = None + shutdown: Callable|None = None name: str def __init__(self, router: APIRouter, reset: Callable, startup: Callable, shutdown: Callable, name:str): diff --git a/backend/utils/models.py b/backend/utils/models.py index 46128e7..962c0b1 100644 --- a/backend/utils/models.py +++ b/backend/utils/models.py @@ -17,7 +17,7 @@ class PasswordChangeForm(BaseModel): class ChangePasswordModel(BaseModel): status: str - access_token: str|None + access_token: str|None = None class IpInterface(BaseModel): addr: str diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 70ca16f..a6e4831 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,15 +5,17 @@ import { ImCross } from 'react-icons/im'; import { Outlet, Route, Routes } from 'react-router-dom'; import MainLayout from './components/MainLayout'; import { PasswordSend, ServerStatusResponse } from './js/models'; -import { errorNotify, fireUpdateRequest, getstatus, HomeRedirector, IS_DEV, login, setpassword } from './js/utils'; +import { DEV_IP_BACKEND, errorNotify, fireUpdateRequest, getstatus, HomeRedirector, IS_DEV, login, setpassword } from './js/utils'; import NFRegex from './pages/NFRegex'; import io from 'socket.io-client'; import RegexProxy from './pages/RegexProxy'; import ServiceDetailsNFRegex from './pages/NFRegex/ServiceDetails'; import ServiceDetailsProxyRegex from './pages/RegexProxy/ServiceDetails'; import PortHijack from './pages/PortHijack'; +import { Firewall } from './pages/Firewall'; -const socket = io({transports: ["websocket", "polling"], path:"/sock", host:IS_DEV?"127.0.0.1:4444":undefined }); + +const socket = IS_DEV?io("ws://"+DEV_IP_BACKEND, {transports: ["websocket", "polling"], path:"/sock" }):io({transports: ["websocket", "polling"], path:"/sock"}); function App() { @@ -154,6 +156,7 @@ function App() { } > } /> + } /> } /> } /> diff --git a/frontend/src/components/Firewall/utils.ts b/frontend/src/components/Firewall/utils.ts new file mode 100644 index 0000000..3906a64 --- /dev/null +++ b/frontend/src/components/Firewall/utils.ts @@ -0,0 +1,70 @@ +import { RegexAddForm, RegexFilter, ServerResponse } from "../../js/models" +import { getapi, postapi } from "../../js/utils" + +export type GeneralStats = { + rules:number, +} + +export enum Protocol { + TCP = "tcp", + UDP = "udp", + ANY = "any" +} + +export enum ActionType { + ACCEPT = "accept", + DROP = "drop", + REJECT = "reject" +} + +export enum RuleMode { + OUT = "O", + IN = "I", +} + +export type Rule = { + active: boolean + name:string, + proto: Protocol, + ip_src: string, + ip_dst: string, + port_src_from: number, + port_dst_from: number, + port_src_to: number, + port_dst_to: number, + action: ActionType, + mode: RuleMode, +} + +export type RuleInfo = { + rules: Rule[] + policy: ActionType +} + + +export type ServerResponseListed = { + status:(ServerResponse & {rule_id:number})[]|string, +} + + +export const firewall = { + stats: async () => { + return await getapi("firewall/stats") as GeneralStats; + }, + rules: async() => { + return await getapi("firewall/rules") as RuleInfo; + }, + rulenable: async (rule_id:number) => { + return await getapi(`firewall/rule/${rule_id}/enable`) as ServerResponse; + }, + ruledisable: async (rule_id:number) => { + return await getapi(`firewall/rule/${rule_id}/disable`) as ServerResponse; + }, + rulerename: async (rule_id:number, name: string) => { + const { status } = await postapi(`firewall/rule/${rule_id}/rename`,{ name }) as ServerResponse; + return status === "ok"?undefined:status + }, + servicesadd: async (data:RuleInfo) => { + return await postapi("firewall/rules/set", data) as ServerResponseListed; + } +} \ No newline at end of file diff --git a/frontend/src/components/NavBar/index.tsx b/frontend/src/components/NavBar/index.tsx index b6bf486..c340747 100644 --- a/frontend/src/components/NavBar/index.tsx +++ b/frontend/src/components/NavBar/index.tsx @@ -1,13 +1,14 @@ -import { Group, MantineColor, Navbar, ScrollArea, Text, ThemeIcon, Title, UnstyledButton } from "@mantine/core"; -import React from "react"; +import { Box, Collapse, Divider, Group, MantineColor, Navbar, ScrollArea, Text, ThemeIcon, Title, UnstyledButton } from "@mantine/core"; +import { useState } from "react"; import { IoMdGitNetwork } from "react-icons/io"; -import { MdTransform } from "react-icons/md"; +import { MdOutlineExpandLess, MdOutlineExpandMore, MdTransform } from "react-icons/md"; import { useNavigate } from "react-router-dom"; import { getmainpath } from "../../js/utils"; import { GrDirections } from "react-icons/gr"; +import { PiWallLight } from "react-icons/pi"; -function NavBarButton({ navigate, closeNav, name, icon, color, disabled }: - { navigate: string, closeNav: () => void, name:string, icon:any, color:MantineColor, disabled?:boolean }) { +function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }: + { navigate?: string, closeNav: () => void, name:string, icon:any, color:MantineColor, disabled?:boolean, onClick?:CallableFunction }) { const navigator = useNavigate() return ({ @@ -22,7 +23,10 @@ function NavBarButton({ navigate, closeNav, name, icon, color, disabled }: backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[0], }, - })} onClick={()=>{navigator(`/${navigate}`);closeNav()}} disabled={disabled}> + })} onClick={()=>{ + if(navigate){navigator(`/${navigate}`);closeNav()} + if (onClick) onClick() + }} disabled={disabled}> {icon} @@ -33,16 +37,26 @@ function NavBarButton({ navigate, closeNav, name, icon, color, disabled }: } export default function NavBar({ closeNav, opened }: {closeNav: () => void, opened: boolean}) { + const [toggleState, setToggleState] = useState(false); + const advancedPaths = ["regexproxy"] + const advancedSelected = advancedPaths.includes(getmainpath()) + const toggle = (toggleState||advancedSelected) return diff --git a/frontend/src/components/PortHijack/utils.ts b/frontend/src/components/PortHijack/utils.ts index 12cbe4a..71fa552 100644 --- a/frontend/src/components/PortHijack/utils.ts +++ b/frontend/src/components/PortHijack/utils.ts @@ -25,10 +25,7 @@ export type ServiceAddForm = { ip_dst: string, } -export type ServiceAddResponse = { - status: string, - service_id?: string, -} +export type ServiceAddResponse = ServerResponse & { service_id: string } export const porthijack = { stats: async () => { diff --git a/frontend/src/components/PortInput.tsx b/frontend/src/components/PortInput.tsx index 58d11ef..2440b9f 100644 --- a/frontend/src/components/PortInput.tsx +++ b/frontend/src/components/PortInput.tsx @@ -6,15 +6,15 @@ interface PortInputProps extends NumberInputProps { } const PortInput = React.forwardRef( (props, ref) => { - const [oldValue, setOldValue] = useState(props.defaultValue?props.defaultValue.toString():"") + const {fullWidth, ...propeties} = props return { const value = parseInt((e.target as HTMLInputElement).value) if (value > 65535) { @@ -28,7 +28,7 @@ const PortInput = React.forwardRef( (props, r props.onInput?.(e) }} ref={ref} - {...props} + {...propeties} /> }) diff --git a/frontend/src/js/utils.tsx b/frontend/src/js/utils.tsx index 1805850..47882db 100644 --- a/frontend/src/js/utils.tsx +++ b/frontend/src/js/utils.tsx @@ -16,10 +16,12 @@ export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}| 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 DEV_IP_BACKEND = "127.0.0.1:4444" + export async function getapi(path:string):Promise{ return await new Promise((resolve, reject) => { - fetch(`${IS_DEV?"http://127.0.0.1:4444":""}/api/${path}`,{ + fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`,{ credentials: "same-origin", headers: { "Authorization" : "Bearer " + window.localStorage.getItem("access_token")} }).then(res => { @@ -35,7 +37,7 @@ export async function getapi(path:string):Promise{ export async function postapi(path:string,data:any,is_form:boolean=false):Promise{ return await new Promise((resolve, reject) => { - fetch(`${IS_DEV?"http://127.0.0.1:4444":""}/api/${path}`, { + fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, { method: 'POST', credentials: "same-origin", cache: 'no-cache', diff --git a/frontend/src/pages/Firewall/index.tsx b/frontend/src/pages/Firewall/index.tsx new file mode 100644 index 0000000..0df70da --- /dev/null +++ b/frontend/src/pages/Firewall/index.tsx @@ -0,0 +1,55 @@ +import { ActionIcon, Badge, LoadingOverlay, Space, Title, Tooltip } from "@mantine/core" +import { useEffect, useState } from "react"; +import { BsPlusLg } from "react-icons/bs" +import { ActionType, GeneralStats, RuleInfo, firewall } from "../../components/Firewall/utils"; +import { errorNotify, eventUpdateName, fireUpdateRequest } from "../../js/utils"; +import { useWindowEvent } from "@mantine/hooks"; + + + +export const Firewall = () => { + + const [generalStats, setGeneralStats] = useState({ rules: 0 }); + const [rules, setRules] = useState({rules:[], policy:ActionType.ACCEPT}); + const [loader, setLoader] = useState(true); + const [tooltipAddOpened, setTooltipAddOpened] = useState(false); + const [open, setOpen] = useState(false); + + + const updateInfo = async () => { + + await Promise.all([ + firewall.stats().then(res => { + setGeneralStats(res) + }).catch( + err => errorNotify("General Info Auto-Update failed!", err.toString()) + ), + firewall.rules().then(res => { + setRules(res) + }).catch(err => { + errorNotify("Home Page Auto-Update failed!", err.toString()) + }) + ]) + setLoader(false) + } + + useWindowEvent(eventUpdateName, updateInfo) + useEffect(fireUpdateRequest,[]) + + + return <> + +
+ Firewall Rules +
+ Rules: {generalStats.rules} + + + setOpen(true)} size="lg" radius="md" variant="filled" + onFocus={() => setTooltipAddOpened(false)} onBlur={() => setTooltipAddOpened(false)} + onMouseEnter={() => setTooltipAddOpened(true)} onMouseLeave={() => setTooltipAddOpened(false)}> + +
+ + +} \ No newline at end of file