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

@@ -1,210 +1,210 @@
name: Create and publish Docker images name: Create and publish Docker images
on: on:
release: release:
types: types:
- published - published
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
jobs: jobs:
docker_build: docker_build:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
include: include:
- os: ubuntu-latest - os: ubuntu-latest
arch: amd64 arch: amd64
run_tests: true run_tests: true
- os: ubuntu-24.04-arm - os: ubuntu-24.04-arm
arch: arm64 arch: arm64
run_tests: true run_tests: true
permissions: permissions:
contents: read contents: read
packages: write packages: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Convert repository name to lowercase - name: Convert repository name to lowercase
id: lowercase id: lowercase
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Build and run firegex - name: Build and run firegex
if: matrix.run_tests if: matrix.run_tests
run: python3 run.py start -P testpassword run: python3 run.py start -P testpassword
- name: Run tests - name: Run tests
if: matrix.run_tests if: matrix.run_tests
run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@master uses: docker/setup-buildx-action@master
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }} images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}
- name: Extract tag name - name: Extract tag name
id: tag id: tag
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
- name: Update version in setup.py - name: Update version in setup.py
run: >- run: >-
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" backend/utils/__init__.py; sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" backend/utils/__init__.py;
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py; sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py; sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
builder: ${{ steps.buildx.outputs.name }} builder: ${{ steps.buildx.outputs.name }}
platforms: linux/${{ matrix.arch }} platforms: linux/${{ matrix.arch }}
push: true push: true
tags: | tags: |
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }} ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }}
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }} ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.arch }} cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.arch }} cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
provenance: false provenance: false
sbom: false sbom: false
docker_manifest: docker_manifest:
needs: docker_build needs: docker_build
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write packages: write
steps: steps:
- name: Convert repository name to lowercase - name: Convert repository name to lowercase
id: lowercase id: lowercase
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract tag name - name: Extract tag name
id: tag id: tag
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
- name: Create and push multi-platform manifest - name: Create and push multi-platform manifest
run: | run: |
# Create manifest list for specific tag # Create manifest list for specific tag
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \ docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \ --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64
# Annotate the manifest with architecture info # Annotate the manifest with architecture info
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \ docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \ ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
--arch amd64 --os linux --arch amd64 --os linux
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \ docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 \ ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 \
--arch arm64 --os linux --arch arm64 --os linux
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}
# Create manifest list for latest tag # Create manifest list for latest tag
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \ docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \ --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 --amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64
# Annotate the latest manifest with architecture info # Annotate the latest manifest with architecture info
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \ docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \ ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
--arch amd64 --os linux --arch amd64 --os linux
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \ docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \ ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \
--arch arm64 --os linux --arch arm64 --os linux
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest
create-rootfs-assets: create-rootfs-assets:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [docker_manifest] needs: [docker_manifest]
permissions: permissions:
contents: write contents: write
packages: read packages: read
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Convert repository name to lowercase - name: Convert repository name to lowercase
id: lowercase id: lowercase
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@master uses: docker/setup-qemu-action@master
with: with:
platforms: all platforms: all
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@master uses: docker/setup-buildx-action@master
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Get latest release tag - name: Get latest release tag
id: get_tag id: get_tag
run: | run: |
LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name') LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Latest release tag: $LATEST_TAG" echo "Latest release tag: $LATEST_TAG"
- name: Export rootfs for amd64 - name: Export rootfs for amd64
run: | run: |
echo "Creating and exporting amd64 container..." echo "Creating and exporting amd64 container..."
CONTAINER_ID=$(docker create --platform linux/amd64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }}) CONTAINER_ID=$(docker create --platform linux/amd64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar" docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar"
docker rm $CONTAINER_ID docker rm $CONTAINER_ID
echo "Compressing amd64 rootfs..." echo "Compressing amd64 rootfs..."
gzip firegex-rootfs-amd64.tar gzip firegex-rootfs-amd64.tar
ls -lh firegex-rootfs-amd64.tar.gz ls -lh firegex-rootfs-amd64.tar.gz
- name: Export rootfs for arm64 - name: Export rootfs for arm64
run: | run: |
echo "Creating and exporting arm64 container..." echo "Creating and exporting arm64 container..."
CONTAINER_ID=$(docker create --platform linux/arm64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }}) CONTAINER_ID=$(docker create --platform linux/arm64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar" docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar"
docker rm $CONTAINER_ID docker rm $CONTAINER_ID
echo "Compressing arm64 rootfs..." echo "Compressing arm64 rootfs..."
gzip firegex-rootfs-arm64.tar gzip firegex-rootfs-arm64.tar
ls -lh firegex-rootfs-arm64.tar.gz ls -lh firegex-rootfs-arm64.tar.gz
- name: Upload rootfs assets to release - name: Upload rootfs assets to release
run: | run: |
echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..." echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..."
gh release upload ${{ steps.get_tag.outputs.tag }} \ gh release upload ${{ steps.get_tag.outputs.tag }} \
firegex-rootfs-amd64.tar.gz \ firegex-rootfs-amd64.tar.gz \
firegex-rootfs-arm64.tar.gz \ firegex-rootfs-arm64.tar.gz \
--clobber --clobber
echo "Assets uploaded successfully!" echo "Assets uploaded successfully!"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,46 +1,46 @@
# This workflow will upload a Python Package using Twine when a release is created # # This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries # # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub. # # This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by # # They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support # # separate terms of service, privacy policy, and support
# documentation. # # documentation.
name: Upload Python Package (fgex alias) # name: Upload Python Package (fgex alias)
on: # on:
release: # release:
types: # types:
- published # - published
permissions: # permissions:
contents: read # contents: read
jobs: # jobs:
deploy: # deploy:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
steps: # steps:
- uses: actions/checkout@v4 # - uses: actions/checkout@v4
- name: Set up Python # - name: Set up Python
uses: actions/setup-python@v5 # uses: actions/setup-python@v5
with: # with:
python-version: '3.x' # python-version: '3.x'
- name: Install dependencies # - name: Install dependencies
run: | # run: |
python -m pip install --upgrade pip # python -m pip install --upgrade pip
pip install build # pip install build
- name: Extract tag name # - name: Extract tag name
id: tag # id: tag
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT # run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
- name: Update version in setup.py # - name: Update version in setup.py
run: >- # run: >-
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py; # sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
- name: Build package # - name: Build package
run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../ # run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
- name: Publish package # - name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 # uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with: # with:
user: __token__ # user: __token__
password: ${{ secrets.PYPI_API_TOKEN_FGEX }} # password: ${{ secrets.PYPI_API_TOKEN_FGEX }}

View File

@@ -1,47 +1,47 @@
# This workflow will upload a Python Package using Twine when a release is created # # This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries # # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub. # # This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by # # They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support # # separate terms of service, privacy policy, and support
# documentation. # # documentation.
name: Upload Python Package # name: Upload Python Package
on: # on:
release: # release:
types: # types:
- published # - published
permissions: # permissions:
contents: read # contents: read
jobs: # jobs:
deploy: # deploy:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
steps: # steps:
- uses: actions/checkout@v4 # - uses: actions/checkout@v4
- name: Set up Python # - name: Set up Python
uses: actions/setup-python@v5 # uses: actions/setup-python@v5
with: # with:
python-version: '3.x' # python-version: '3.x'
- name: Install dependencies # - name: Install dependencies
run: | # run: |
python -m pip install --upgrade pip # python -m pip install --upgrade pip
pip install build # pip install build
- name: Extract tag name # - name: Extract tag name
id: tag # id: tag
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT # run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
- name: Update version in setup.py # - name: Update version in setup.py
run: >- # run: >-
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py; # sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py; # sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
- name: Build package # - name: Build package
run: cd fgex-lib && python -m build && mv ./dist ../ # run: cd fgex-lib && python -m build && mv ./dist ../
- name: Publish package # - name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 # uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with: # with:
user: __token__ # user: __token__
password: ${{ secrets.PYPI_API_TOKEN }} # password: ${{ secrets.PYPI_API_TOKEN }}

View File

@@ -1,49 +1,52 @@
# Firegex Dockerfile UUID signature # Firegex Dockerfile UUID signature
# cf1795af-3284-4183-a888-81ad3590ad84 # cf1795af-3284-4183-a888-81ad3590ad84
# Needed for run.py to detect the Dockerfile # Needed for run.py to detect the Dockerfile
FROM --platform=$BUILDPLATFORM oven/bun AS frontend FROM --platform=$BUILDPLATFORM oven/bun AS frontend
WORKDIR /app WORKDIR /app
ADD ./frontend/package.json . ADD ./frontend/package.json .
ADD ./frontend/bun.lock . ADD ./frontend/bun.lock .
RUN bun i RUN bun i
COPY ./frontend/ . COPY ./frontend/ .
RUN bun run build RUN bun run build
# Base fedora container # Base Ubuntu container
FROM --platform=$TARGETARCH quay.io/fedora/fedora:43 AS base FROM --platform=$TARGETARCH ubuntu:24.04 AS base
RUN dnf -y update && dnf install -y python3.14 libnetfilter_queue \ RUN apt-get update && apt-get install -y python3 libnetfilter-queue1 \
libnfnetlink libmnl libcap-ng-utils nftables \ libnfnetlink0 libmnl0 libcap-ng-utils nftables \
vectorscan libtins python3-nftables libpcap && dnf clean all libhs5 libtins4.4 python3-nftables libpcap0.8 && \
apt-get clean && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /execute/modules
WORKDIR /execute RUN mkdir -p /execute/modules
WORKDIR /execute
FROM --platform=$TARGETARCH base AS compiler
FROM --platform=$TARGETARCH base AS compiler
RUN dnf -y update && dnf install -y python3.14-devel @development-tools gcc-c++ \
libnetfilter_queue-devel libnfnetlink-devel libmnl-devel \ RUN apt-get update && apt-get install -y python3-dev build-essential g++ \
vectorscan-devel libtins-devel libpcap-devel boost-devel libnetfilter-queue-dev libnfnetlink-dev libmnl-dev \
libhyperscan-dev libtins-dev libpcap-dev libboost-dev pkg-config && \
COPY ./backend/binsrc /execute/binsrc apt-get clean && rm -rf /var/lib/apt/lists/*
RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.14 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3) COPY ./backend/binsrc /execute/binsrc
RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
#Building main conteiner RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.12 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
FROM --platform=$TARGETARCH base AS final
#Building main conteiner
COPY ./backend/requirements.txt /execute/requirements.txt FROM --platform=$TARGETARCH base AS final
COPY ./fgex-lib /execute/fgex-lib
COPY ./backend/requirements.txt /execute/requirements.txt
RUN dnf -y update && dnf install -y gcc-c++ python3.14-devel uv git &&\ COPY ./fgex-lib /execute/fgex-lib
uv pip install --no-cache --system ./fgex-lib &&\
uv pip install --no-cache --system -r /execute/requirements.txt &&\ RUN apt-get update && apt-get install -y g++ python3-dev python3-pip git && \
uv cache clean && dnf remove -y gcc-c++ python3.14-devel uv git && dnf clean all pip3 install --no-cache-dir --break-system-packages ./fgex-lib && \
pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt && \
COPY ./backend/ /execute/ apt-get remove -y g++ python3-dev git && \
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/ apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --from=frontend /app/dist/ ./frontend/
COPY ./backend/ /execute/
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"] COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
COPY --from=frontend /app/dist/ ./frontend/
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"]

View File

@@ -5,6 +5,7 @@ import asyncio
import traceback import traceback
from fastapi import HTTPException from fastapi import HTTPException
import time import time
import json
from utils import run_func from utils import run_func
from utils import DEBUG from utils import DEBUG
from utils import nicenessify from utils import nicenessify
@@ -35,11 +36,12 @@ class FiregexInterceptor:
self.last_time_exception = 0 self.last_time_exception = 0
self.outstrem_function = None self.outstrem_function = None
self.expection_function = None self.expection_function = None
self.traffic_function = None
self.outstrem_task: asyncio.Task self.outstrem_task: asyncio.Task
self.outstrem_buffer = "" self.outstrem_buffer = ""
@classmethod @classmethod
async def start(cls, srv: Service, outstream_func=None, exception_func=None): async def start(cls, srv: Service, outstream_func=None, exception_func=None, traffic_func=None):
self = cls() self = cls()
self.srv = srv self.srv = srv
self.filter_map_lock = asyncio.Lock() self.filter_map_lock = asyncio.Lock()
@@ -47,6 +49,7 @@ class FiregexInterceptor:
self.sock_conn_lock = asyncio.Lock() self.sock_conn_lock = asyncio.Lock()
self.outstrem_function = outstream_func self.outstrem_function = outstream_func
self.expection_function = exception_func self.expection_function = exception_func
self.traffic_function = traffic_func
if not self.sock_conn_lock.locked(): if not self.sock_conn_lock.locked():
await self.sock_conn_lock.acquire() await self.sock_conn_lock.acquire()
self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock" self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock"
@@ -83,6 +86,16 @@ class FiregexInterceptor:
self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n" self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n"
if self.outstrem_function: if self.outstrem_function:
await run_func(self.outstrem_function, self.srv.id, out_data) await run_func(self.outstrem_function, self.srv.id, out_data)
# Parse JSON traffic events (if binary emits them)
if self.traffic_function:
for line in out_data.splitlines():
if line.startswith("{"): # JSON event from binary
try:
event = json.loads(line)
if "ts" in event and "verdict" in event: # Basic validation
await run_func(self.traffic_function, self.srv.id, event)
except (json.JSONDecodeError, KeyError):
pass # Ignore malformed JSON, keep backward compat with raw logs
async def _start_binary(self): async def _start_binary(self):
proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy")) proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))

View File

@@ -1,4 +1,5 @@
import asyncio import asyncio
from collections import deque
from modules.nfproxy.firegex import FiregexInterceptor from modules.nfproxy.firegex import FiregexInterceptor
from modules.nfproxy.nftables import FiregexTables, FiregexFilter from modules.nfproxy.nftables import FiregexTables, FiregexFilter
from modules.nfproxy.models import Service, PyFilter from modules.nfproxy.models import Service, PyFilter
@@ -12,7 +13,7 @@ class STATUS:
nft = FiregexTables() nft = FiregexTables()
class ServiceManager: class ServiceManager:
def __init__(self, srv: Service, db, outstream_func=None, exception_func=None): def __init__(self, srv: Service, db, outstream_func=None, exception_func=None, traffic_func=None):
self.srv = srv self.srv = srv
self.db = db self.db = db
self.status = STATUS.STOP self.status = STATUS.STOP
@@ -21,11 +22,17 @@ class ServiceManager:
self.interceptor = None self.interceptor = None
self.outstream_function = outstream_func self.outstream_function = outstream_func
self.last_exception_time = 0 self.last_exception_time = 0
self.traffic_events = deque(maxlen=500) # Ring buffer for traffic viewer
async def excep_internal_handler(srv, exc_time): async def excep_internal_handler(srv, exc_time):
self.last_exception_time = exc_time self.last_exception_time = exc_time
if exception_func: if exception_func:
await run_func(exception_func, srv, exc_time) await run_func(exception_func, srv, exc_time)
self.exception_function = excep_internal_handler self.exception_function = excep_internal_handler
async def traffic_internal_handler(srv, event):
self.traffic_events.append(event)
if traffic_func:
await run_func(traffic_func, srv, event)
self.traffic_function = traffic_internal_handler
async def _update_filters_from_db(self): async def _update_filters_from_db(self):
pyfilters = [ pyfilters = [
@@ -69,7 +76,7 @@ class ServiceManager:
async def start(self): async def start(self):
if not self.interceptor: if not self.interceptor:
nft.delete(self.srv) nft.delete(self.srv)
self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function) self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function, traffic_func=self.traffic_function)
await self._update_filters_from_db() await self._update_filters_from_db()
self._set_status(STATUS.ACTIVE) self._set_status(STATUS.ACTIVE)
@@ -87,14 +94,24 @@ class ServiceManager:
async def update_filters(self): async def update_filters(self):
async with self.lock: async with self.lock:
await self._update_filters_from_db() await self._update_filters_from_db()
def get_traffic_events(self, limit: int = 500):
"""Return recent traffic events from ring buffer"""
events_list = list(self.traffic_events)
return events_list[-limit:] if limit < len(events_list) else events_list
def clear_traffic_events(self):
"""Clear traffic event history"""
self.traffic_events.clear()
class FirewallManager: class FirewallManager:
def __init__(self, db:SQLite, outstream_func=None, exception_func=None): def __init__(self, db:SQLite, outstream_func=None, exception_func=None, traffic_func=None):
self.db = db self.db = db
self.service_table: dict[str, ServiceManager] = {} self.service_table: dict[str, ServiceManager] = {}
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
self.outstream_function = outstream_func self.outstream_function = outstream_func
self.exception_function = exception_func self.exception_function = exception_func
self.traffic_function = traffic_func
async def close(self): async def close(self):
for key in list(self.service_table.keys()): for key in list(self.service_table.keys()):
@@ -116,7 +133,7 @@ class FirewallManager:
srv = Service.from_dict(srv) srv = Service.from_dict(srv)
if srv.id in self.service_table: if srv.id in self.service_table:
continue continue
self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function) self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function, traffic_func=self.traffic_function)
await self.service_table[srv.id].next(srv.status) await self.service_table[srv.id].next(srv.status)
def get(self,srv_id) -> ServiceManager: def get(self,srv_id) -> ServiceManager:

View File

@@ -1,8 +1,8 @@
fastapi[all] fastapi[all]
httpx httpx
uvicorn[standard] uvicorn[standard]
psutil psutil
python-jose[cryptography] python-jose[cryptography]
python-socketio python-socketio
brotli brotli
#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py #git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py

View File

@@ -113,6 +113,8 @@ async def startup():
utils.socketio.on("nfproxy-outstream-leave", leave_outstream) utils.socketio.on("nfproxy-outstream-leave", leave_outstream)
utils.socketio.on("nfproxy-exception-join", join_exception) utils.socketio.on("nfproxy-exception-join", join_exception)
utils.socketio.on("nfproxy-exception-leave", leave_exception) utils.socketio.on("nfproxy-exception-leave", leave_exception)
utils.socketio.on("nfproxy-traffic-join", join_traffic)
utils.socketio.on("nfproxy-traffic-leave", leave_traffic)
async def shutdown(): async def shutdown():
db.backup() db.backup()
@@ -133,7 +135,10 @@ async def outstream_func(service_id, data):
async def exception_func(service_id, timestamp): async def exception_func(service_id, timestamp):
await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}") await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}")
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func) async def traffic_func(service_id, event):
await utils.socketio.emit(f"nfproxy-traffic-{service_id}", event, room=f"nfproxy-traffic-{service_id}")
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func, traffic_func=traffic_func)
@app.get('/services', response_model=list[ServiceModel]) @app.get('/services', response_model=list[ServiceModel])
async def get_service_list(): async def get_service_list():
@@ -368,6 +373,28 @@ async def get_pyfilters_code(service_id: str):
except FileNotFoundError: except FileNotFoundError:
return "" return ""
@app.get('/services/{service_id}/traffic')
async def get_traffic_events(service_id: str, limit: int = 500):
"""Get recent traffic events from the service ring buffer"""
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
raise HTTPException(status_code=400, detail="This service does not exists!")
try:
events = firewall.get(service_id).get_traffic_events(limit)
return {"events": events, "count": len(events)}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post('/services/{service_id}/traffic/clear', response_model=StatusMessageModel)
async def clear_traffic_events(service_id: str):
"""Clear traffic event history for a service"""
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
raise HTTPException(status_code=400, detail="This service does not exists!")
try:
firewall.get(service_id).clear_traffic_events()
return {"status": "ok"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
#Socket io events #Socket io events
async def join_outstream(sid, data): async def join_outstream(sid, data):
"""Client joins a room.""" """Client joins a room."""
@@ -397,3 +424,20 @@ async def leave_exception(sid, data):
if srv: if srv:
await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}") await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}")
async def join_traffic(sid, data):
"""Client joins traffic viewer room and gets initial event history."""
srv = data.get("service")
if srv:
room = f"nfproxy-traffic-{srv}"
await utils.socketio.enter_room(sid, room)
try:
events = firewall.get(srv).get_traffic_events(500)
await utils.socketio.emit("nfproxy-traffic-history", {"events": events}, room=sid)
except Exception:
pass # Service may not exist or not started
async def leave_traffic(sid, data):
"""Client leaves traffic viewer room."""
srv = data.get("service")
if srv:
await utils.socketio.leave_room(sid, f"nfproxy-traffic-{srv}")

View File

@@ -1,221 +1,221 @@
import asyncio import asyncio
from ipaddress import ip_address, ip_interface from ipaddress import ip_address, ip_interface
import os import os
import socket import socket
import psutil import psutil
import sys import sys
import nftables import nftables
from socketio import AsyncServer from socketio import AsyncServer
from fastapi import Path from fastapi import Path
from typing import Annotated from typing import Annotated
from functools import wraps from functools import wraps
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
import traceback import traceback
from utils.models import StatusMessageModel from utils.models import StatusMessageModel
from typing import List from typing import List
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1")) LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
socketio:AsyncServer = None socketio:AsyncServer = None
sid_list:set = set() sid_list:set = set()
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers") ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
ON_DOCKER = "DOCKER" in sys.argv ON_DOCKER = "DOCKER" in sys.argv
DEBUG = "DEBUG" in sys.argv DEBUG = "DEBUG" in sys.argv
NORELOAD = "NORELOAD" in sys.argv NORELOAD = "NORELOAD" in sys.argv
FIREGEX_PORT = int(os.getenv("PORT","4444")) FIREGEX_PORT = int(os.getenv("PORT","4444"))
FIREGEX_HOST = os.getenv("HOST","0.0.0.0") FIREGEX_HOST = os.getenv("HOST","0.0.0.0")
FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None) FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None)
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
JWT_ALGORITHM: str = "HS256" JWT_ALGORITHM: str = "HS256"
API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0" API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
PortType = Annotated[int, Path(gt=0, lt=65536)] PortType = Annotated[int, Path(gt=0, lt=65536)]
async def run_func(func, *args, **kwargs): async def run_func(func, *args, **kwargs):
if asyncio.iscoroutinefunction(func): if asyncio.iscoroutinefunction(func):
return await func(*args, **kwargs) return await func(*args, **kwargs)
else: else:
return func(*args, **kwargs) return func(*args, **kwargs)
async def socketio_emit(elements:list[str]): async def socketio_emit(elements:list[str]):
await socketio.emit("update",elements) await socketio.emit("update",elements)
def refactor_name(name:str): def refactor_name(name:str):
name = name.strip() name = name.strip()
while " " in name: while " " in name:
name = name.replace(" "," ") name = name.replace(" "," ")
return name return name
class SysctlManager: class SysctlManager:
def __init__(self, ctl_table): def __init__(self, ctl_table):
self.old_table = {} self.old_table = {}
self.new_table = {} self.new_table = {}
if os.path.isdir("/sys_host/"): if os.path.isdir("/sys_host/"):
self.old_table = dict() self.old_table = dict()
self.new_table = dict(ctl_table) self.new_table = dict(ctl_table)
for name in ctl_table.keys(): for name in ctl_table.keys():
self.old_table[name] = read_sysctl(name) self.old_table[name] = read_sysctl(name)
def write_table(self, table) -> bool: def write_table(self, table) -> bool:
for name, value in table.items(): for name, value in table.items():
if read_sysctl(name) != value: if read_sysctl(name) != value:
write_sysctl(name, value) write_sysctl(name, value)
def set(self): def set(self):
self.write_table(self.new_table) self.write_table(self.new_table)
def reset(self): def reset(self):
self.write_table(self.old_table) self.write_table(self.old_table)
def read_sysctl(name:str): def read_sysctl(name:str):
with open(f"/sys_host/{name}", "rt") as f: with open(f"/sys_host/{name}", "rt") as f:
return "1" in f.read() return "1" in f.read()
def write_sysctl(name:str, value:bool): def write_sysctl(name:str, value:bool):
with open(f"/sys_host/{name}", "wt") as f: with open(f"/sys_host/{name}", "wt") as f:
f.write("1" if value else "0") f.write("1" if value else "0")
def list_files(mypath): def list_files(mypath):
from os import listdir from os import listdir
from os.path import isfile, join from os.path import isfile, join
return [f for f in listdir(mypath) if isfile(join(mypath, f))] return [f for f in listdir(mypath) if isfile(join(mypath, f))]
def ip_parse(ip:str): def ip_parse(ip:str):
return str(ip_interface(ip).network) return str(ip_interface(ip).network)
def is_ip_parse(ip:str): def is_ip_parse(ip:str):
try: try:
ip_parse(ip) ip_parse(ip)
return True return True
except Exception: except Exception:
return False return False
def addr_parse(ip:str): def addr_parse(ip:str):
return str(ip_address(ip)) return str(ip_address(ip))
def ip_family(ip:str): def ip_family(ip:str):
return "ip6" if ip_interface(ip).version == 6 else "ip" return "ip6" if ip_interface(ip).version == 6 else "ip"
def get_interfaces(): def get_interfaces():
def _get_interfaces(): def _get_interfaces():
for int_name, interfs in psutil.net_if_addrs().items(): for int_name, interfs in psutil.net_if_addrs().items():
for interf in interfs: for interf in interfs:
if interf.family in [socket.AF_INET, socket.AF_INET6]: if interf.family in [socket.AF_INET, socket.AF_INET6]:
yield {"name": int_name, "addr":interf.address} yield {"name": int_name, "addr":interf.address}
return list(_get_interfaces()) return list(_get_interfaces())
def nftables_int_to_json(ip_int): def nftables_int_to_json(ip_int):
ip_int = ip_parse(ip_int) ip_int = ip_parse(ip_int)
ip_addr = str(ip_int).split("/")[0] ip_addr = str(ip_int).split("/")[0]
ip_addr_cidr = int(str(ip_int).split("/")[1]) ip_addr_cidr = int(str(ip_int).split("/")[1])
return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}} return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}
def nftables_json_to_int(ip_json_int): def nftables_json_to_int(ip_json_int):
if isinstance(ip_json_int,str): if isinstance(ip_json_int,str):
return str(ip_parse(ip_json_int)) return str(ip_parse(ip_json_int))
else: else:
return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}' return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}'
class Singleton(object): class Singleton(object):
__instance = None __instance = None
def __new__(class_, *args, **kwargs): def __new__(class_, *args, **kwargs):
if not isinstance(class_.__instance, class_): if not isinstance(class_.__instance, class_):
class_.__instance = object.__new__(class_, *args, **kwargs) class_.__instance = object.__new__(class_, *args, **kwargs)
return class_.__instance return class_.__instance
class NFTableManager(Singleton): class NFTableManager(Singleton):
table_name = "firegex" table_name = "firegex"
def __init__(self, init_cmd, reset_cmd): def __init__(self, init_cmd, reset_cmd):
self.__init_cmds = init_cmd self.__init_cmds = init_cmd
self.__reset_cmds = reset_cmd self.__reset_cmds = reset_cmd
self.nft = nftables.Nftables() self.nft = nftables.Nftables()
def raw_cmd(self, *cmds): def raw_cmd(self, *cmds):
return self.nft.json_cmd({"nftables": list(cmds)}) return self.nft.json_cmd({"nftables": list(cmds)})
def cmd(self, *cmds): def cmd(self, *cmds):
code, out, err = self.raw_cmd(*cmds) code, out, err = self.raw_cmd(*cmds)
if code == 0: if code == 0:
return out return out
else: else:
raise Exception(err) raise Exception(err)
def init(self): def init(self):
self.reset() self.reset()
self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}}) self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}})
self.cmd(*self.__init_cmds) self.cmd(*self.__init_cmds)
def reset(self): def reset(self):
self.raw_cmd(*self.__reset_cmds) self.raw_cmd(*self.__reset_cmds)
def list_rules(self, tables = None, chains = None): def list_rules(self, tables = None, chains = None):
for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]: for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]:
if tables and filter["table"] not in tables: if tables and filter["table"] not in tables:
continue continue
if chains and filter["chain"] not in chains: if chains and filter["chain"] not in chains:
continue continue
yield filter yield filter
def raw_list(self): def raw_list(self):
return self.cmd({"list": {"ruleset": None}})["nftables"] return self.cmd({"list": {"ruleset": None}})["nftables"]
def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"): def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"):
res = obj.model_dump(mode=mode, exclude_unset=not unset) res = obj.model_dump(mode=mode, exclude_unset=not unset)
if convert_keys: if convert_keys:
for from_k, to_k in convert_keys.items(): for from_k, to_k in convert_keys.items():
if from_k in res: if from_k in res:
res[to_k] = res.pop(from_k) res[to_k] = res.pop(from_k)
if exclude: if exclude:
for ele in exclude: for ele in exclude:
if ele in res: if ele in res:
del res[ele] del res[ele]
return res return res
def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict: def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict:
if isinstance(obj, list): if isinstance(obj, list):
return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj] return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj]
return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode)
def register_event(sio_server: AsyncServer, event_name: str, model: BaseModel, response_model: BaseModel|None = None): def register_event(sio_server: AsyncServer, event_name: str, model: BaseModel, response_model: BaseModel|None = None):
def decorator(func): def decorator(func):
@sio_server.on(event_name) # Automatically registers the event @sio_server.on(event_name) # Automatically registers the event
@wraps(func) @wraps(func)
async def wrapper(sid, data): async def wrapper(sid, data):
try: try:
# Parse and validate incoming data # Parse and validate incoming data
parsed_data = model.model_validate(data) parsed_data = model.model_validate(data)
except ValidationError: except ValidationError:
return json_like(StatusMessageModel(status=f"Invalid {event_name} request")) return json_like(StatusMessageModel(status=f"Invalid {event_name} request"))
# Call the original function with the parsed data # Call the original function with the parsed data
result = await func(sid, parsed_data) result = await func(sid, parsed_data)
# If a response model is provided, validate the output # If a response model is provided, validate the output
if response_model: if response_model:
try: try:
parsed_result = response_model.model_validate(result) parsed_result = response_model.model_validate(result)
except ValidationError: except ValidationError:
traceback.print_exc() traceback.print_exc()
return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response")) return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response"))
else: else:
parsed_result = result parsed_result = result
# Emit the validated result # Emit the validated result
if parsed_result: if parsed_result:
if isinstance(parsed_result, BaseModel): if isinstance(parsed_result, BaseModel):
return json_like(parsed_result) return json_like(parsed_result)
return parsed_result return parsed_result
return wrapper return wrapper
return decorator return decorator
def nicenessify(priority:int, pid:int|None=None): def nicenessify(priority:int, pid:int|None=None):
try: try:
pid = os.getpid() if pid is None else pid pid = os.getpid() if pid is None else pid
ps = psutil.Process(pid) ps = psutil.Process(pid)
if os.name == 'posix': if os.name == 'posix':
ps.nice(priority) ps.nice(priority)
except Exception as e: except Exception as e:
print(f"Error setting priority: {e} {traceback.format_exc()}") print(f"Error setting priority: {e} {traceback.format_exc()}")
pass pass

116
docs/TRAFFIC_VIEWER.md Normal file
View File

@@ -0,0 +1,116 @@
# Traffic Viewer - JSON Event Format
The traffic viewer is now fully integrated. To enable structured event display, the NFProxy C++ binary (`backend/binsrc/nfproxy.cpp`) should emit JSON lines to stdout with the following format:
## JSON Event Schema
```json
{
"ts": 1701964234567,
"direction": "in",
"src_ip": "192.168.1.100",
"src_port": 54321,
"dst_ip": "10.0.0.5",
"dst_port": 443,
"proto": "tcp",
"size": 1420,
"verdict": "accept",
"filter": "filter_sanitize",
"sample_hex": "474554202f20485454502f312e310d0a486f73743a206578616d706c652e636f6d..."
}
```
## Fields
- `ts` (required): Unix timestamp in milliseconds
- `direction`: `"in"` (client→server) or `"out"` (server→client)
- `src_ip`, `dst_ip`: Source and destination IP addresses
- `src_port`, `dst_port`: Source and destination ports
- `proto`: Protocol name (e.g., `"tcp"`, `"udp"`)
- `size`: Packet/payload size in bytes
- `verdict` (required): `"accept"`, `"drop"`, `"reject"`, or `"edited"`
- `filter`: Name of the Python filter that processed this packet
- `sample_hex`: Hex-encoded sample of payload (first 64-128 bytes recommended)
## Implementation Notes
1. **Backward Compatibility**: The parser in `firegex.py::_stream_handler` only processes lines starting with `{`. Non-JSON output (logs, ACK messages) continues to work as before.
2. **Performance**: Emit JSON only when needed. Consider an env flag:
```cpp
bool emit_traffic_json = getenv("FIREGEX_TRAFFIC_JSON") != nullptr;
if (emit_traffic_json) {
std::cout << json_event << std::endl;
}
```
3. **Sample Code** (C++ with nlohmann/json or similar):
```cpp
#include <nlohmann/json.hpp>
using json = nlohmann::json;
void emit_traffic_event(const PacketInfo& pkt, const char* verdict, const char* filter_name) {
json event = {
{"ts", current_timestamp_ms()},
{"direction", pkt.is_inbound ? "in" : "out"},
{"src_ip", pkt.src_addr},
{"src_port", pkt.src_port},
{"dst_ip", pkt.dst_addr},
{"dst_port", pkt.dst_port},
{"proto", pkt.protocol},
{"size", pkt.payload_len},
{"verdict", verdict},
{"filter", filter_name},
{"sample_hex", hex_encode(pkt.payload, std::min(64, pkt.payload_len))}
};
std::cout << event.dump() << std::endl;
}
```
## Testing Without Binary Changes
The viewer works immediately—it will display "No traffic events yet" until the binary is updated. You can manually test the Socket.IO flow by emitting mock events from Python:
```python
# In backend shell or script
import asyncio
import json
from utils import socketio
async def emit_test_event():
event = {
"ts": int(time.time() * 1000),
"direction": "in",
"src_ip": "192.168.1.50",
"src_port": 12345,
"dst_ip": "10.0.0.1",
"dst_port": 80,
"proto": "tcp",
"size": 512,
"verdict": "accept",
"filter": "test_filter"
}
await socketio.emit("nfproxy-traffic-YOUR_SERVICE_ID", event, room="nfproxy-traffic-YOUR_SERVICE_ID")
```
## Current Features
✅ **Backend**:
- Ring buffer stores last 500 events per service
- REST endpoint: `GET /api/nfproxy/services/{id}/traffic?limit=500`
- REST endpoint: `POST /api/nfproxy/services/{id}/traffic/clear`
- Socket.IO channels: `nfproxy-traffic-{service_id}` for live events, `nfproxy-traffic-history` on join
✅ **Frontend**:
- Live table view at `/nfproxy/{service_id}/traffic`
- Client-side text filter (searches IP, verdict, filter name, proto)
- Click row to view full event details + hex payload
- Auto-scroll, clear history button
- Accessible via new button (double-arrow icon) in ServiceDetails page
## Next Steps
1. Update `backend/binsrc/nfproxy.cpp` to emit JSON events as shown above
2. Rebuild the C++ binary
3. Start a service and generate traffic—viewer will populate in real-time
4. Optionally add more filters (by verdict, time range) or export to PCAP

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,175 +1,182 @@
import { PyFilter, ServerResponse } from "../../js/models" import { PyFilter, ServerResponse } from "../../js/models"
import { deleteapi, getapi, postapi, putapi } from "../../js/utils" import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
export type Service = { export type Service = {
service_id:string, service_id:string,
name:string, name:string,
status:string, status:string,
port:number, port:number,
proto: string, proto: string,
ip_int: string, ip_int: string,
n_filters:number, n_filters:number,
edited_packets:number, edited_packets:number,
blocked_packets:number, blocked_packets:number,
fail_open:boolean, fail_open:boolean,
} }
export type ServiceAddForm = { export type ServiceAddForm = {
name:string, name:string,
port:number, port:number,
proto:string, proto:string,
ip_int:string, ip_int:string,
fail_open: boolean, fail_open: boolean,
} }
export type ServiceSettings = { export type ServiceSettings = {
port?:number, port?:number,
ip_int?:string, ip_int?:string,
fail_open?: boolean, fail_open?: boolean,
} }
export type ServiceAddResponse = { export type ServiceAddResponse = {
status: string, status: string,
service_id?: string, service_id?: string,
} }
export const serviceQueryKey = ["nfproxy","services"] export const serviceQueryKey = ["nfproxy","services"]
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services}) export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({ export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
queryKey:[...serviceQueryKey,service_id,"pyfilters"], queryKey:[...serviceQueryKey,service_id,"pyfilters"],
queryFn:() => nfproxy.servicepyfilters(service_id) queryFn:() => nfproxy.servicepyfilters(service_id)
}) })
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({ export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"], queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
queryFn:() => nfproxy.getpyfilterscode(service_id) queryFn:() => nfproxy.getpyfilterscode(service_id)
}) })
export const nfproxy = { export const nfproxy = {
services: async () => { services: async () => {
return await getapi("nfproxy/services") as Service[]; return await getapi("nfproxy/services") as Service[];
}, },
serviceinfo: async (service_id:string) => { serviceinfo: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}`) as Service; return await getapi(`nfproxy/services/${service_id}`) as Service;
}, },
pyfilterenable: async (service_id:string, filter_name:string) => { pyfilterenable: async (service_id:string, filter_name:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse; const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
}, },
pyfilterdisable: async (service_id:string, filter_name:string) => { pyfilterdisable: async (service_id:string, filter_name:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse; const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
}, },
servicestart: async (service_id:string) => { servicestart: async (service_id:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse; const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
}, },
servicerename: async (service_id:string, name: string) => { servicerename: async (service_id:string, name: string) => {
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse; const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
}, },
servicestop: async (service_id:string) => { servicestop: async (service_id:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse; const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
}, },
servicesadd: async (data:ServiceAddForm) => { servicesadd: async (data:ServiceAddForm) => {
return await postapi("nfproxy/services",data) as ServiceAddResponse; return await postapi("nfproxy/services",data) as ServiceAddResponse;
}, },
servicedelete: async (service_id:string) => { servicedelete: async (service_id:string) => {
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse; const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
}, },
servicepyfilters: async (service_id:string) => { servicepyfilters: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[]; return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
}, },
settings: async (service_id:string, data:ServiceSettings) => { settings: async (service_id:string, data:ServiceSettings) => {
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse; const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
}, },
getpyfilterscode: async (service_id:string) => { getpyfilterscode: async (service_id:string) => {
return await getapi(`nfproxy/services/${service_id}/code`) as string; return await getapi(`nfproxy/services/${service_id}/code`) as string;
}, },
setpyfilterscode: async (service_id:string, code:string) => { setpyfilterscode: async (service_id:string, code:string) => {
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse; const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
return status === "ok"?undefined:status 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 };
},
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol cleartraffic: async (service_id:string) => {
const { status } = await postapi(`nfproxy/services/${service_id}/traffic/clear`) as ServerResponse;
# From here we can import the DataTypes that we want to use: return status === "ok"?undefined:status
# 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 export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
# - This code will be executed once at the TCP stream start
# - The filter will be called for each packet in the stream # From here we can import the DataTypes that we want to use:
# - You can store in global context some data you need, but exceeding with data stored could be dangerous # The data type must be specified in the filter functions
# - At the end of the stream the global context will be destroyed # And will also interally be used to decide when call some filters and how aggregate data
from firegex.nfproxy.models import RawPacket
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 # global context in this execution is dedicated to a single TCP stream
# - This code will be executed once at the TCP stream start
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP # - The filter will be called for each packet in the stream
# - The filter must return one of the following values: # - You can store in global context some data you need, but exceeding with data stored could be dangerous
# - ACCEPT: The packet will be accepted # - At the end of the stream the global context will be destroyed
# - 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 from firegex.nfproxy import pyfilter
# - DROP: All the packets in this stream will be easly dropped # pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
# If you want, you can use print to debug your filters, but this could slow down the filter from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
# - The filter must return one of the following values:
# Filter names must be unique and are specified by the name of the function wrapped by the decorator # - ACCEPT: The packet will be accepted
@pyfilter # - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream)
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction # - UNSTABLE_MANGLE: The packet will be mangled and accepted
def strange_filter(packet:RawPacket): # - DROP: All the packets in this stream will be easly dropped
# 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: # If you want, you can use print to debug your filters, but this could slow down the filter
# 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 # Filter names must be unique and are specified by the name of the function wrapped by the decorator
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only @pyfilter
if b"TEST_MANGLING" in packet.l4_data: # This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead def strange_filter(packet:RawPacket):
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE") # Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
return UNSTABLE_MANGLE # Also is not garanteed that l4_data is the same of the packet data:
# Drops the traffic # packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
if b"BAD DATA 1" in packet.data: # Unorder packets in TCP are accepted by default, and python is not called in this case
return DROP # For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
# Rejects the traffic if b"TEST_MANGLING" in packet.l4_data:
if b"BAD DATA 2" in packet.data: # It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
return REJECT packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
# Accepts the traffic (default if None is returned) return UNSTABLE_MANGLE
return ACCEPT # Drops the traffic
if b"BAD DATA 1" in packet.data:
# Example with a higher level of abstraction return DROP
@pyfilter # Rejects the traffic
def http_filter(http:HTTPRequest): if b"BAD DATA 2" in packet.data:
if http.method == "GET" and "test" in http.url: return REJECT
return REJECT # Accepts the traffic (default if None is returned)
return ACCEPT
# ADVANCED OPTIONS
# You can specify some additional options on the streaming managment # Example with a higher level of abstraction
# pyproxy will automatically store all the packets (already ordered by the c++ binary): @pyfilter
# def http_filter(http:HTTPRequest):
# If the stream is too big, you can specify what actions to take: if http.method == "GET" and "test" in http.url:
# This can be done defining some variables in the global context return REJECT
# - 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 # ADVANCED OPTIONS
# Only types required by at least 1 filter will be stored. # You can specify some additional options on the streaming managment
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full # pyproxy will automatically store all the packets (already ordered by the c++ binary):
# - 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 # If the stream is too big, you can specify what actions to take:
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter # This can be done defining some variables in the global context
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic # - 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
from firege.nfproxy import FullStreamAction # Only types required by at least 1 filter will be stored.
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
# Example of a global context # - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
FGEX_STREAM_MAX_SIZE = 4096 # - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT # - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic # - 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 { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils'; import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
import { ImCross } from "react-icons/im" import { ImCross } from "react-icons/im"
import { nfregex, Service } from './utils'; import { nfregex, Service } from './utils';
import PortAndInterface from '../PortAndInterface'; import PortAndInterface from '../PortAndInterface';
import { IoMdInformationCircleOutline } from "react-icons/io"; import { IoMdInformationCircleOutline } from "react-icons/io";
import { ServiceAddForm as ServiceAddFormOriginal } from './utils'; import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean} type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) { function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
const initialValues = { const initialValues = {
name: "", name: "",
port:edit?.port??8080, port:edit?.port??8080,
ip_int:edit?.ip_int??"", ip_int:edit?.ip_int??"",
proto:edit?.proto??"tcp", proto:edit?.proto??"tcp",
fail_open: edit?.fail_open??false, fail_open: edit?.fail_open??false,
autostart: true autostart: true
} }
const form = useForm({ const form = useForm({
initialValues: initialValues, initialValues: initialValues,
validate:{ validate:{
name: (value) => edit? null : value !== "" ? null : "Service name is required", name: (value) => edit? null : value !== "" ? null : "Service name is required",
port: (value) => (value>0 && value<65536) ? null : "Invalid port", port: (value) => (value>0 && value<65536) ? null : "Invalid port",
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol", proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address", ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
} }
}) })
useEffect(() => { useEffect(() => {
if (opened){ if (opened){
form.setInitialValues(initialValues) form.setInitialValues(initialValues)
form.reset() form.reset()
} }
}, [opened]) }, [opened])
const close = () =>{ const close = () =>{
onClose() onClose()
form.reset() form.reset()
setError(null) setError(null)
} }
const [submitLoading, setSubmitLoading] = useState(false) const [submitLoading, setSubmitLoading] = useState(false)
const [error, setError] = useState<string|null>(null) const [error, setError] = useState<string|null>(null)
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{ const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
setSubmitLoading(true) setSubmitLoading(true)
if (edit){ if (edit){
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => { nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
if (!res){ if (!res){
setSubmitLoading(false) setSubmitLoading(false)
close(); close();
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`) okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
} }
}).catch( err => { }).catch( err => {
setSubmitLoading(false) setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]") setError("Request Failed! [ "+err+" ]")
}) })
}else{ }else{
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => { nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
if (res.status === "ok" && res.service_id){ if (res.status === "ok" && res.service_id){
setSubmitLoading(false) setSubmitLoading(false)
close(); close();
if (autostart) nfregex.servicestart(res.service_id) if (autostart) nfregex.servicestart(res.service_id)
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`) okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
}else{ }else{
setSubmitLoading(false) setSubmitLoading(false)
setError("Invalid request! [ "+res.status+" ]") setError("Invalid request! [ "+res.status+" ]")
} }
}).catch( err => { }).catch( err => {
setSubmitLoading(false) setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]") 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> 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)}> <form onSubmit={form.onSubmit(submitRequest)}>
{!edit?<TextInput {!edit?<TextInput
label="Service name" label="Service name"
placeholder="Challenge 01" placeholder="Challenge 01"
{...form.getInputProps('name')} {...form.getInputProps('name')}
/>:null} />:null}
<Space h="md" /> <Space h="md" />
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} /> <PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
<Space h="md" /> <Space h="md" />
<Box className='center-flex'> <Box className='center-flex'>
<Box> <Box>
{!edit?<Switch {!edit?<Switch
label="Auto-Start Service" label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })} {...form.getInputProps('autostart', { type: 'checkbox' })}
/>:null} />:null}
<Space h="sm" /> <Space h="sm" />
<Switch <Switch
label={<Box className='center-flex'> label={<Box className='center-flex'>
Enable fail-open nfqueue Enable fail-open nfqueue
<Space w="xs" /> <Space w="xs" />
<Tooltip label={<> <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 /> 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} /> <IoMdInformationCircleOutline size={15} />
</Tooltip> </Tooltip>
</Box>} </Box>}
{...form.getInputProps('fail_open', { type: 'checkbox' })} {...form.getInputProps('fail_open', { type: 'checkbox' })}
/> />
</Box> </Box>
<Box className="flex-spacer"></Box> <Box className="flex-spacer"></Box>
<SegmentedControl <SegmentedControl
data={[ data={[
{ label: 'TCP', value: 'tcp' }, { label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' }, { label: 'UDP', value: 'udp' },
]} ]}
{...form.getInputProps('proto')} {...form.getInputProps('proto')}
/> />
</Box> </Box>
<Group justify='flex-end' mt="md" mb="sm"> <Group justify='flex-end' mt="md" mb="sm">
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button> <Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
</Group> </Group>
{error?<> {error?<>
<Space h="md" /> <Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}> <Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error} Error: {error}
</Notification><Space h="md" /> </Notification><Space h="md" />
</>:null} </>:null}
</form> </form>
</Modal> </Modal>
} }
export default AddEditService; export default AddEditService;

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import { PiWallLight } from "react-icons/pi";
import { useNavbarStore } from "../../js/store"; import { useNavbarStore } from "../../js/store";
import { getMainPath } from "../../js/utils"; import { getMainPath } from "../../js/utils";
import { BsRegex } from "react-icons/bs"; import { BsRegex } from "react-icons/bs";
import { MdVisibility } from "react-icons/md";
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }: function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) { { 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="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="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="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"> {/* <Box px="xs" mt="lg">
<Title order={5}>Experimental Features 🧪</Title> <Title order={5}>Experimental Features 🧪</Title>
</Box> </Box>

View File

@@ -1,113 +1,113 @@
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core'; import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { useState } from 'react'; import { useState } from 'react';
import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils'; import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils';
import { ImCross } from "react-icons/im" import { ImCross } from "react-icons/im"
import { porthijack } from './utils'; import { porthijack } from './utils';
import PortAndInterface from '../PortAndInterface'; import PortAndInterface from '../PortAndInterface';
type ServiceAddForm = { type ServiceAddForm = {
name:string, name:string,
public_port:number, public_port:number,
proxy_port:number, proxy_port:number,
proto:string, proto:string,
ip_src:string, ip_src:string,
ip_dst:string, ip_dst:string,
autostart: boolean, autostart: boolean,
} }
function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) { function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) {
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
name:"", name:"",
public_port:80, public_port:80,
proxy_port:8080, proxy_port:8080,
proto:"tcp", proto:"tcp",
ip_src:"", ip_src:"",
ip_dst:"127.0.0.1", ip_dst:"127.0.0.1",
autostart: false, autostart: false,
}, },
validate:{ validate:{
name: (value) => value !== ""? null : "Service name is required", name: (value) => value !== ""? null : "Service name is required",
public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port", public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port",
proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port", proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port",
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol", 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_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", ip_dst: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid destination IP address",
} }
}) })
const close = () =>{ const close = () =>{
onClose() onClose()
form.reset() form.reset()
setError(null) setError(null)
} }
const [submitLoading, setSubmitLoading] = useState(false) const [submitLoading, setSubmitLoading] = useState(false)
const [error, setError] = useState<string|null>(null) const [error, setError] = useState<string|null>(null)
const submitRequest = ({ name, proxy_port, public_port, autostart, proto, ip_src, ip_dst }:ServiceAddForm) =>{ const submitRequest = ({ name, proxy_port, public_port, autostart, proto, ip_src, ip_dst }:ServiceAddForm) =>{
setSubmitLoading(true) setSubmitLoading(true)
porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).then( res => { porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).then( res => {
if (res.status === "ok" && res.service_id){ if (res.status === "ok" && res.service_id){
setSubmitLoading(false) setSubmitLoading(false)
close(); close();
if (autostart) porthijack.servicestart(res.service_id) if (autostart) porthijack.servicestart(res.service_id)
okNotify(`Service ${name} has been added`, `Successfully added service from port ${public_port} to ${proxy_port}`) okNotify(`Service ${name} has been added`, `Successfully added service from port ${public_port} to ${proxy_port}`)
}else{ }else{
setSubmitLoading(false) setSubmitLoading(false)
setError("Invalid request! [ "+res.status+" ]") setError("Invalid request! [ "+res.status+" ]")
} }
}).catch( err => { }).catch( err => {
setSubmitLoading(false) setSubmitLoading(false)
setError("Request Failed! [ "+err+" ]") setError("Request Failed! [ "+err+" ]")
}) })
} }
return <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered> return <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered>
<form onSubmit={form.onSubmit(submitRequest)}> <form onSubmit={form.onSubmit(submitRequest)}>
<TextInput <TextInput
label="Service name" label="Service name"
placeholder="Challenge 01" placeholder="Challenge 01"
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<Space h="md" /> <Space h="md" />
<PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" /> <PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" />
<Space h="md" /> <Space h="md" />
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" /> <PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" />
<Space h="md" /> <Space h="md" />
<Box className='center-flex'> <Box className='center-flex'>
<Switch <Switch
label="Auto-Start Service" label="Auto-Start Service"
{...form.getInputProps('autostart', { type: 'checkbox' })} {...form.getInputProps('autostart', { type: 'checkbox' })}
/> />
<Box className="flex-spacer" /> <Box className="flex-spacer" />
<SegmentedControl <SegmentedControl
data={[ data={[
{ label: 'TCP', value: 'tcp' }, { label: 'TCP', value: 'tcp' },
{ label: 'UDP', value: 'udp' }, { label: 'UDP', value: 'udp' },
]} ]}
{...form.getInputProps('proto')} {...form.getInputProps('proto')}
/> />
</Box> </Box>
<Group justify='flex-end' mt="md" mb="sm"> <Group justify='flex-end' mt="md" mb="sm">
<Button loading={submitLoading} type="submit">Add Service</Button> <Button loading={submitLoading} type="submit">Add Service</Button>
</Group> </Group>
{error?<> {error?<>
<Space h="md" /> <Space h="md" />
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}> <Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
Error: {error} Error: {error}
</Notification><Space h="md" /> </Notification><Space h="md" />
</>:null} </>:null}
</form> </form>
</Modal> </Modal>
} }
export default AddNewService; export default AddNewService;

View File

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

View File

@@ -1,64 +1,64 @@
import { ServerResponse } from "../../js/models" import { ServerResponse } from "../../js/models"
import { deleteapi, getapi, postapi, putapi } from "../../js/utils" import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
export type GeneralStats = { export type GeneralStats = {
services:number services:number
} }
export type Service = { export type Service = {
name:string, name:string,
service_id:string, service_id:string,
active:boolean, active:boolean,
proto: string, proto: string,
ip_src: string, ip_src: string,
ip_dst: string, ip_dst: string,
proxy_port: number, proxy_port: number,
public_port: number, public_port: number,
} }
export type ServiceAddForm = { export type ServiceAddForm = {
name:string, name:string,
public_port:number, public_port:number,
proxy_port:number, proxy_port:number,
proto:string, proto:string,
ip_src: string, ip_src: string,
ip_dst: string, ip_dst: string,
} }
export type ServiceAddResponse = ServerResponse & { service_id: string } export type ServiceAddResponse = ServerResponse & { service_id: string }
export const queryKey = ["porthijack","services"] export const queryKey = ["porthijack","services"]
export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services}) export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services})
export const porthijack = { export const porthijack = {
services: async () : Promise<Service[]> => { services: async () : Promise<Service[]> => {
return await getapi("porthijack/services") as Service[]; return await getapi("porthijack/services") as Service[];
}, },
serviceinfo: async (service_id:string) => { serviceinfo: async (service_id:string) => {
return await getapi(`porthijack/services/${service_id}`) as Service; return await getapi(`porthijack/services/${service_id}`) as Service;
}, },
servicestart: async (service_id:string) => { servicestart: async (service_id:string) => {
const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse; const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
}, },
servicerename: async (service_id:string, name: string) => { servicerename: async (service_id:string, name: string) => {
const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse; const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
}, },
servicestop: async (service_id:string) => { servicestop: async (service_id:string) => {
const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse; const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
}, },
servicesadd: async (data:ServiceAddForm) => { servicesadd: async (data:ServiceAddForm) => {
return await postapi("porthijack/services",data) as ServiceAddResponse; return await postapi("porthijack/services",data) as ServiceAddResponse;
}, },
servicedelete: async (service_id:string) => { servicedelete: async (service_id:string) => {
const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse; const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse;
return status === "ok"?undefined:status return status === "ok"?undefined:status
}, },
changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => { 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; 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 { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { PyFilter } from '../../js/models'; import { PyFilter } from '../../js/models';
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils'; import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
import { FaPause, FaPlay } from 'react-icons/fa'; import { FaPause, FaPlay } from 'react-icons/fa';
import { FaFilter } from "react-icons/fa"; import { FaFilter } from "react-icons/fa";
import { nfproxy } from '../NFProxy/utils'; import { nfproxy } from '../NFProxy/utils';
import { FaPencilAlt } from 'react-icons/fa'; import { FaPencilAlt } from 'react-icons/fa';
export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) { export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) {
const isMedium = isMediumScreen() const isMedium = isMediumScreen()
const changeRegexStatus = () => { const changeRegexStatus = () => {
(filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => { (filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => {
if(!res){ if(!res){
okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`) okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`)
}else{ }else{
errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`) errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
} }
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`)) }).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
} }
return <Box my="sm" display="flex" style={{alignItems:"center"}}> return <Box my="sm" display="flex" style={{alignItems:"center"}}>
<Box className="firegex__regexview__pyfilter_text" style={{ width: "100%", alignItems: "center"}} display="flex" > <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" /> <Badge size="sm" radius="lg" mr="sm" color={filterInfo.active?"lime":"red"} variant="filled" />
{filterInfo.name} {filterInfo.name}
<Box className='flex-spacer' /> <Box className='flex-spacer' />
<Space w="xs" /> <Space w="xs" />
{isMedium?<> {isMedium?<>
<Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge> <Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge>
<Space w="xs" /> <Space w="xs" />
<Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge> <Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge>
<Space w="lg" /> <Space w="lg" />
</>:null} </>:null}
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"}> <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"> <ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="lg" radius="md" variant="filled">
{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon> {filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
</Tooltip> </Tooltip>
</Box> </Box>
</Box> </Box>
} }

View File

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

View File

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

View File

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

View File

@@ -1,237 +1,243 @@
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core'; import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
import { Navigate, useNavigate, useParams } from 'react-router'; import { Navigate, useNavigate, useParams } from 'react-router';
import { Badge, Divider, Menu } from '@mantine/core'; import { Badge, Divider, Menu } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa'; import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils'; import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
import { MdDoubleArrow } from "react-icons/md" import { MdDoubleArrow } from "react-icons/md"
import YesNoModal from '../../components/YesNoModal'; import YesNoModal from '../../components/YesNoModal';
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils'; import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
import { BsTrashFill } from 'react-icons/bs'; import { BsTrashFill } from 'react-icons/bs';
import { BiRename } from 'react-icons/bi' import { BiRename } from 'react-icons/bi'
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm'; import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
import { MenuDropDownWithButton } from '../../components/MainLayout'; import { MenuDropDownWithButton } from '../../components/MainLayout';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { FaArrowLeft } from "react-icons/fa"; import { FaArrowLeft } from "react-icons/fa";
import { IoSettingsSharp } from 'react-icons/io5'; import { IoSettingsSharp } from 'react-icons/io5';
import AddEditService from '../../components/NFProxy/AddEditService'; import AddEditService from '../../components/NFProxy/AddEditService';
import PyFilterView from '../../components/PyFilterView'; import PyFilterView from '../../components/PyFilterView';
import { TbPlugConnected } from 'react-icons/tb'; import { TbPlugConnected } from 'react-icons/tb';
import { CodeHighlight } from '@mantine/code-highlight'; import { CodeHighlight } from '@mantine/code-highlight';
import { FaPython } from "react-icons/fa"; import { FaPython } from "react-icons/fa";
import { FiFileText } from "react-icons/fi"; import { FiFileText } from "react-icons/fi";
import { ModalLog } from '../../components/ModalLog'; import { ModalLog } from '../../components/ModalLog';
import { useListState } from '@mantine/hooks'; import { useListState } from '@mantine/hooks';
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning'; import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
import { DocsButton } from '../../components/DocsButton'; import { DocsButton } from '../../components/DocsButton';
export default function ServiceDetailsNFProxy() { export default function ServiceDetailsNFProxy() {
const {srv} = useParams() const {srv} = useParams()
const services = nfproxyServiceQuery() const services = nfproxyServiceQuery()
const serviceInfo = services.data?.find(s => s.service_id == srv) const serviceInfo = services.data?.find(s => s.service_id == srv)
const filtersList = nfproxyServicePyfiltersQuery(srv??"") const filtersList = nfproxyServicePyfiltersQuery(srv??"")
const [deleteModal, setDeleteModal] = useState(false) const [deleteModal, setDeleteModal] = useState(false)
const [renameModal, setRenameModal] = useState(false) const [renameModal, setRenameModal] = useState(false)
const [editModal, setEditModal] = useState(false) const [editModal, setEditModal] = useState(false)
const [buttonLoading, setButtonLoading] = useState(false) const [buttonLoading, setButtonLoading] = useState(false)
const queryClient = useQueryClient() const queryClient = useQueryClient()
const filterCode = nfproxyServiceFilterCodeQuery(srv??"") const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
const navigate = useNavigate() const navigate = useNavigate()
const isMedium = isMediumScreen() const isMedium = isMediumScreen()
const [openLogModal, setOpenLogModal] = useState(false) const [openLogModal, setOpenLogModal] = useState(false)
const [logData, logDataSetters] = useListState<string>([]); const [logData, logDataSetters] = useListState<string>([]);
useEffect(()=>{ useEffect(()=>{
if (srv){ if (srv){
if (openLogModal){ if (openLogModal){
logDataSetters.setState([]) logDataSetters.setState([])
socketio.emit("nfproxy-outstream-join", { service: srv }); socketio.emit("nfproxy-outstream-join", { service: srv });
socketio.on(`nfproxy-outstream-${srv}`, (data) => { socketio.on(`nfproxy-outstream-${srv}`, (data) => {
logDataSetters.append(data) logDataSetters.append(data)
}); });
}else{ }else{
socketio.emit("nfproxy-outstream-leave", { service: srv }); socketio.emit("nfproxy-outstream-leave", { service: srv });
socketio.off(`nfproxy-outstream-${srv}`); socketio.off(`nfproxy-outstream-${srv}`);
logDataSetters.setState([]) logDataSetters.setState([])
} }
return () => { return () => {
socketio.emit("nfproxy-outstream-leave", { service: srv }); socketio.emit("nfproxy-outstream-leave", { service: srv });
socketio.off(`nfproxy-outstream-${srv}`); socketio.off(`nfproxy-outstream-${srv}`);
logDataSetters.setState([]) logDataSetters.setState([])
} }
} }
}, [openLogModal, srv]) }, [openLogModal, srv])
if (services.isLoading) return <LoadingOverlay visible={true} /> if (services.isLoading) return <LoadingOverlay visible={true} />
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace /> if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
let status_color = "gray"; let status_color = "gray";
switch(serviceInfo.status){ switch(serviceInfo.status){
case "stop": status_color = "red"; break; case "stop": status_color = "red"; break;
case "active": status_color = "teal"; break; case "active": status_color = "teal"; break;
} }
const startService = async () => { const startService = async () => {
setButtonLoading(true) setButtonLoading(true)
await nfproxy.servicestart(serviceInfo.service_id).then(res => { await nfproxy.servicestart(serviceInfo.service_id).then(res => {
if(!res){ if(!res){
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`) okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
queryClient.invalidateQueries(serviceQueryKey) queryClient.invalidateQueries(serviceQueryKey)
}else{ }else{
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`) errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
} }
}).catch(err => { }).catch(err => {
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`) errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
}) })
setButtonLoading(false) setButtonLoading(false)
} }
const deleteService = () => { const deleteService = () => {
nfproxy.servicedelete(serviceInfo.service_id).then(res => { nfproxy.servicedelete(serviceInfo.service_id).then(res => {
if (!res){ if (!res){
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`) okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
queryClient.invalidateQueries(serviceQueryKey) queryClient.invalidateQueries(serviceQueryKey)
}else }else
errorNotify("An error occurred while deleting a service",`Error: ${res}`) errorNotify("An error occurred while deleting a service",`Error: ${res}`)
}).catch(err => { }).catch(err => {
errorNotify("An error occurred while deleting a service",`Error: ${err}`) errorNotify("An error occurred while deleting a service",`Error: ${err}`)
}) })
} }
const stopService = async () => { const stopService = async () => {
setButtonLoading(true) setButtonLoading(true)
await nfproxy.servicestop(serviceInfo.service_id).then(res => { await nfproxy.servicestop(serviceInfo.service_id).then(res => {
if(!res){ if(!res){
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`) okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
queryClient.invalidateQueries(serviceQueryKey) queryClient.invalidateQueries(serviceQueryKey)
}else{ }else{
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`) errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
} }
}).catch(err => { }).catch(err => {
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`) errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
}) })
setButtonLoading(false); setButtonLoading(false);
} }
return <> return <>
<LoadingOverlay visible={filtersList.isLoading} /> <LoadingOverlay visible={filtersList.isLoading} />
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg"> <Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Box> <Box>
<Title order={1}> <Title order={1}>
<Box className="center-flex"> <Box className="center-flex">
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name} <MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
</Box> </Box>
</Title> </Title>
</Box> </Box>
{isMedium?null:<Space h="md" />} {isMedium?null:<Space h="md" />}
<Box className='center-flex'> <Box className='center-flex'>
<ExceptionWarning service_id={srv} /> <ExceptionWarning service_id={srv} />
<Space w="sm" /> <Space w="sm" />
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm"> <Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
{serviceInfo.status} {serviceInfo.status}
</Badge> </Badge>
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm"> <Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
:{serviceInfo.port} :{serviceInfo.port}
</Badge> </Badge>
<MenuDropDownWithButton> <MenuDropDownWithButton>
<Menu.Item><b>Edit service</b></Menu.Item> <Menu.Item><b>Edit service</b></Menu.Item>
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</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> <Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
<Divider /> <Divider />
<Menu.Label><b>Danger zone</b></Menu.Label> <Menu.Label><b>Danger zone</b></Menu.Label>
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item> <Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
</MenuDropDownWithButton> </MenuDropDownWithButton>
<Space w="md"/> <Space w="md"/>
<Tooltip label="Show logs" zIndex={0} color="cyan"> <Tooltip label="Show logs" zIndex={0} color="cyan">
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled"> <ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
<FiFileText size="20px" /> <FiFileText size="20px" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Box> <Space w="xs"/>
</Box> <Tooltip label="Traffic viewer" zIndex={0} color="grape">
{isMedium?null:<Space h="md" />} <ActionIcon color="grape" size="lg" radius="md" onClick={()=>navigate(`/nfproxy/${srv}/traffic`)} variant="filled">
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg"> <MdDoubleArrow size="20px" />
<Box className={isMedium?'center-flex':'center-flex-row'}> </ActionIcon>
<Box className='center-flex'> </Tooltip>
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge> </Box>
<Space w="xs" /> </Box>
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge> {isMedium?null:<Space h="md" />}
<Space w="xs" /> <Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge> <Box className={isMedium?'center-flex':'center-flex-row'}>
</Box> <Box className='center-flex'>
{isMedium?<Space w="xs" />:<Space h="xs" />} <Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
<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> <Space w="xs" />
</Box> <Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
{isMedium?null:<Space h="xl" />} <Space w="xs" />
<Box className='center-flex'> <Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
<Tooltip label="Go back" zIndex={0} color="cyan"> </Box>
<ActionIcon color="cyan" {isMedium?<Space w="xs" />:<Space h="xs" />}
onClick={() => navigate("/")} size="xl" radius="md" variant="filled" <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>
aria-describedby="tooltip-back-id"> </Box>
<FaArrowLeft size="25px" /> {isMedium?null:<Space h="xl" />}
</ActionIcon> <Box className='center-flex'>
</Tooltip> <Tooltip label="Go back" zIndex={0} color="cyan">
<Space w="md"/> <ActionIcon color="cyan"
<Tooltip label="Stop service" zIndex={0} color="red"> onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
<ActionIcon color="red" loading={buttonLoading} aria-describedby="tooltip-back-id">
onClick={stopService} size="xl" radius="md" variant="filled" <FaArrowLeft size="25px" />
disabled={serviceInfo.status === "stop"} </ActionIcon>
aria-describedby="tooltip-stop-id"> </Tooltip>
<FaStop size="20px" /> <Space w="md"/>
</ActionIcon> <Tooltip label="Stop service" zIndex={0} color="red">
</Tooltip> <ActionIcon color="red" loading={buttonLoading}
<Space w="md"/> onClick={stopService} size="xl" radius="md" variant="filled"
<Tooltip label="Start service" zIndex={0} color="teal"> disabled={serviceInfo.status === "stop"}
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading} aria-describedby="tooltip-stop-id">
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}> <FaStop size="20px" />
<FaPlay size="20px" /> </ActionIcon>
</ActionIcon> </Tooltip>
</Tooltip> <Space w="md"/>
</Box> <Tooltip label="Start service" zIndex={0} color="teal">
</Box> <ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
<Divider my="xl" /> <FaPlay size="20px" />
</ActionIcon>
{filterCode.data?<> </Tooltip>
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title> </Box>
<CodeHighlight code={filterCode.data} language="python" mt="lg" /> </Box>
</>: null}
<Divider my="xl" />
{(!filtersList.data || filtersList.data.length == 0)?<>
<Space h="xl" /> {filterCode.data?<>
<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> <Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
<Space h="xs" /> <CodeHighlight code={filterCode.data} language="python" mt="lg" />
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title> </>: null}
<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 || filtersList.data.length == 0)?<>
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</> <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>
<YesNoModal <Space h="xs" />
title='Are you sure to delete this service?' <Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
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! ⚠️`} <Space h="xs" />
onClose={()=>setDeleteModal(false) } <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>
action={deleteService} </>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
opened={deleteModal} }
/> <YesNoModal
<RenameForm title='Are you sure to delete this service?'
onClose={()=>setRenameModal(false)} 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! ⚠️`}
opened={renameModal} onClose={()=>setDeleteModal(false) }
service={serviceInfo} action={deleteService}
/> opened={deleteModal}
<AddEditService />
opened={editModal} <RenameForm
onClose={()=>setEditModal(false)} onClose={()=>setRenameModal(false)}
edit={serviceInfo} opened={renameModal}
/> service={serviceInfo}
<ModalLog />
opened={openLogModal} <AddEditService
close={()=>setOpenLogModal(false)} opened={editModal}
title={`Logs for service ${serviceInfo.name}`} onClose={()=>setEditModal(false)}
data={logData.join("")} 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 { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { BsPlusLg } from "react-icons/bs"; import { BsPlusLg } from "react-icons/bs";
import { useNavigate, useParams } from 'react-router'; import { useNavigate, useParams } from 'react-router';
import ServiceRow from '../../components/NFProxy/ServiceRow'; import ServiceRow from '../../components/NFProxy/ServiceRow';
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils'; import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
import AddEditService from '../../components/NFProxy/AddEditService'; import AddEditService from '../../components/NFProxy/AddEditService';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { TbPlugConnected, TbReload } from 'react-icons/tb'; import { TbPlugConnected, TbReload } from 'react-icons/tb';
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils'; import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa'; import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
import { MdUploadFile } from "react-icons/md"; import { MdUploadFile } from "react-icons/md";
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
import { useFileDialog } from '@mantine/hooks'; import { useFileDialog } from '@mantine/hooks';
import { CodeHighlight } from '@mantine/code-highlight'; import { CodeHighlight } from '@mantine/code-highlight';
import { DocsButton } from '../../components/DocsButton'; import { DocsButton } from '../../components/DocsButton';
export default function NFProxy({ children }: { children: any }) { export default function NFProxy({ children }: { children: any }) {
const navigator = useNavigate() const navigator = useNavigate()
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const {srv} = useParams() const {srv} = useParams()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const isMedium = isMediumScreen() const isMedium = isMediumScreen()
const services = nfproxyServiceQuery() const services = nfproxyServiceQuery()
const fileDialog = useFileDialog({ const fileDialog = useFileDialog({
accept: ".py", accept: ".py",
multiple: false, multiple: false,
resetOnOpen: true, resetOnOpen: true,
onChange: (files) => { onChange: (files) => {
if (files?.length??0 > 0) if (files?.length??0 > 0)
setFile(files![0]) setFile(files![0])
} }
}); });
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
useEffect(() => { useEffect(() => {
if (!srv) return if (!srv) return
const service = services.data?.find(s => s.service_id === srv) const service = services.data?.find(s => s.service_id === srv)
if (!service) return if (!service) return
if (file){ if (file){
console.log("Uploading code") console.log("Uploading code")
const notify_id = notifications.show( const notify_id = notifications.show(
{ {
title: "Uploading code", title: "Uploading code",
message: `Uploading code for service ${service.name}`, message: `Uploading code for service ${service.name}`,
color: "blue", color: "blue",
icon: <MdUploadFile size={20} />, icon: <MdUploadFile size={20} />,
autoClose: false, autoClose: false,
loading: true, loading: true,
} }
) )
file.text() file.text()
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString())) .then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
.then( res => { .then( res => {
if (!res){ if (!res){
notifications.update({ notifications.update({
id: notify_id, id: notify_id,
title: "Code uploaded", title: "Code uploaded",
message: `Successfully uploaded code for service ${service.name}`, message: `Successfully uploaded code for service ${service.name}`,
color: "green", color: "green",
icon: <MdUploadFile size={20} />, icon: <MdUploadFile size={20} />,
autoClose: 5000, autoClose: 5000,
loading: false, loading: false,
}) })
}else{ }else{
notifications.update({ notifications.update({
id: notify_id, id: notify_id,
title: "Code upload failed", title: "Code upload failed",
message: `Error: ${res}`, message: `Error: ${res}`,
color: "red", color: "red",
icon: <MdUploadFile size={20} />, icon: <MdUploadFile size={20} />,
autoClose: 5000, autoClose: 5000,
loading: false, loading: false,
}) })
} }
}).catch( err => { }).catch( err => {
notifications.update({ notifications.update({
id: notify_id, id: notify_id,
title: "Code upload failed", title: "Code upload failed",
message: `Error: ${err}`, message: `Error: ${err}`,
color: "red", color: "red",
icon: <MdUploadFile size={20} />, icon: <MdUploadFile size={20} />,
autoClose: 5000, autoClose: 5000,
loading: false, loading: false,
}) })
}).finally(()=>{setFile(null)}) }).finally(()=>{setFile(null)})
} }
}, [file]) }, [file])
useEffect(()=> { useEffect(()=> {
if(services.isError) if(services.isError)
errorNotify("NFProxy Update failed!", getErrorMessage(services.error)) errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
},[services.isError]) },[services.isError])
const closeModal = () => {setOpen(false);} const closeModal = () => {setOpen(false);}
return <> return <>
<Space h="sm" /> <Space h="sm" />
<Box className={isMedium?'center-flex':'center-flex-row'}> <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> <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" />} {isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex' > <Box className='center-flex' >
{isMedium?"General stats:":null} {isMedium?"General stats:":null}
<Space w="xs" /> <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> <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" /> <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> <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" /> <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> <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" /> <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> <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" /> <Space w="xs" />
</Box> </Box>
{isMedium?null:<Space h="md" />} {isMedium?null:<Space h="md" />}
<Box className='center-flex' > <Box className='center-flex' >
{ srv? { srv?
<Tooltip label="Upload a new filter code" position='bottom' color="blue"> <Tooltip label="Upload a new filter code" position='bottom' color="blue">
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}> <ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
<MdUploadFile size={18} /> <MdUploadFile size={18} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
: <Tooltip label="Add a new service" position='bottom' color="blue"> : <Tooltip label="Add a new service" position='bottom' color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"> <ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
<BsPlusLg size={18} /> <BsPlusLg size={18} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
} }
<Space w="xs" /> <Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo"> <Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}> <ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
<TbReload size={18} /> <TbReload size={18} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" /> <Space w="xs" />
<DocsButton doc="nfproxy" /> <DocsButton doc="nfproxy" />
</Box> </Box>
</Box> </Box>
<Space h="md" /> <Space h="md" />
<Box className="center-flex-row" style={{gap: 20}}> <Box className="center-flex-row" style={{gap: 20}}>
{srv?null:<> {srv?null:<>
<LoadingOverlay visible={services.isLoading} /> <LoadingOverlay visible={services.isLoading} />
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{ {(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
navigator("/nfproxy/"+srv.service_id) navigator("/nfproxy/"+srv.service_id)
}} />):<> }} />):<>
<Box className='center-flex-row'> <Box className='center-flex-row'>
<Space h="xl" /> <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> <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" /> <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> <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" /> <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> <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" /> <Space h="lg" />
<Box className='center-flex' style={{gap: 20}}> <Box className='center-flex' style={{gap: 20}}>
<Tooltip label="Add a new service" color="blue"> <Tooltip label="Add a new service" color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"> <ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
<BsPlusLg size="20px" /> <BsPlusLg size="20px" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<DocsButton doc="nfproxy" size="xl" /> <DocsButton doc="nfproxy" size="xl" />
</Box> </Box>
</Box> </Box>
</>} </>}
</>} </>}
</Box> </Box>
{srv?children:null} {srv?children:null}
{!srv? {!srv?
<AddEditService opened={open} onClose={closeModal} />:null <AddEditService opened={open} onClose={closeModal} />:null
} }
</> </>
} }

View File

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

View File

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

View File

@@ -1,77 +1,77 @@
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core'; import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { BsPlusLg } from "react-icons/bs"; import { BsPlusLg } from "react-icons/bs";
import ServiceRow from '../../components/PortHijack/ServiceRow'; import ServiceRow from '../../components/PortHijack/ServiceRow';
import { porthijackServiceQuery } from '../../components/PortHijack/utils'; import { porthijackServiceQuery } from '../../components/PortHijack/utils';
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils'; import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
import AddNewService from '../../components/PortHijack/AddNewService'; import AddNewService from '../../components/PortHijack/AddNewService';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { TbReload } from 'react-icons/tb'; import { TbReload } from 'react-icons/tb';
import { FaServer } from 'react-icons/fa'; import { FaServer } from 'react-icons/fa';
import { GrDirections } from 'react-icons/gr'; import { GrDirections } from 'react-icons/gr';
import { DocsButton } from '../../components/DocsButton'; import { DocsButton } from '../../components/DocsButton';
function PortHijack() { function PortHijack() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const queryClient = useQueryClient() const queryClient = useQueryClient()
const isMedium = isMediumScreen() const isMedium = isMediumScreen()
const services = porthijackServiceQuery() const services = porthijackServiceQuery()
useEffect(()=>{ useEffect(()=>{
if(services.isError) if(services.isError)
errorNotify("Porthijack Update failed!", getErrorMessage(services.error)) errorNotify("Porthijack Update failed!", getErrorMessage(services.error))
},[services.isError]) },[services.isError])
const closeModal = () => {setOpen(false);} const closeModal = () => {setOpen(false);}
return <> return <>
<Space h="sm" /> <Space h="sm" />
<Box className={isMedium?'center-flex':'center-flex-row'}> <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> <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" />} {isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
<Box className='center-flex'> <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> <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" /> <Space w="xs" />
<Tooltip label="Add a new service" position='bottom' color="blue"> <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> <ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" /> <Space w="xs" />
<Tooltip label="Refresh" position='bottom' color="indigo"> <Tooltip label="Refresh" position='bottom' color="indigo">
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled" <ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
loading={services.isFetching}><TbReload size={18} /></ActionIcon> loading={services.isFetching}><TbReload size={18} /></ActionIcon>
</Tooltip> </Tooltip>
<Space w="xs" /> <Space w="xs" />
<DocsButton doc="porthijack" /> <DocsButton doc="porthijack" />
</Box> </Box>
</Box> </Box>
<Space h="md" /> <Space h="md" />
<Box className="center-flex-row" style={{gap: 20}}> <Box className="center-flex-row" style={{gap: 20}}>
<LoadingOverlay visible={services.isLoading} /> <LoadingOverlay visible={services.isLoading} />
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<> {(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
<Box className='center-flex-row'> <Box className='center-flex-row'>
<Space h="xl" /> <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> <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" /> <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> <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" /> <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> <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" /> <Space h="lg" />
<Box className='center-flex' style={{gap: 20}}> <Box className='center-flex' style={{gap: 20}}>
<Tooltip label="Add a new service" color="blue"> <Tooltip label="Add a new service" color="blue">
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"> <ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
<BsPlusLg size="20px" /> <BsPlusLg size="20px" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<DocsButton doc="porthijack" size="xl" /> <DocsButton doc="porthijack" size="xl" />
</Box> </Box>
</Box> </Box>
</>} </>}
<AddNewService opened={open} onClose={closeModal} /> <AddNewService opened={open} onClose={closeModal} />
</Box> </Box>
</> </>
} }
export default PortHijack; 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": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"types": ["vite/client", "vite-plugin-svgr/client", "node"], "types": ["vite/client", "vite-plugin-svgr/client", "node"],
"allowJs": false, "allowJs": false,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": false, "esModuleInterop": false,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": ["src"] "include": ["src"]
} }

2
run.py
View File

@@ -268,7 +268,7 @@ def write_compose(skip_password = True):
"firewall": { "firewall": {
"restart": "unless-stopped", "restart": "unless-stopped",
"container_name": "firegex", "container_name": "firegex",
"build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}", "build" if g.build else "image": "." if g.build else f"ghcr.io/ilyastar9999/firegex:{args.version}",
"network_mode": "host", "network_mode": "host",
"environment": [ "environment": [
f"PORT={args.port}", f"PORT={args.port}",

View File

@@ -1,51 +1,51 @@
0,4090.616 0,4090.616
1,2211.62 1,2211.62
2,1165.45 2,1165.45
3,849.39 3,849.39
4,828.635 4,828.635
5,741.537 5,741.537
6,632.721 6,632.721
7,624.772 7,624.772
8,529.234 8,529.234
9,469.688 9,469.688
10,336.33 10,336.33
11,427.783 11,427.783
12,400.662 12,400.662
13,335.086 13,335.086
14,342.042 14,342.042
15,307.283 15,307.283
16,239.694 16,239.694
17,295.163 17,295.163
18,285.787 18,285.787
19,254.402 19,254.402
20,250.553 20,250.553
21,227.146 21,227.146
22,238.747 22,238.747
23,234.718 23,234.718
24,210.484 24,210.484
25,210.697 25,210.697
26,205.943 26,205.943
27,202.568 27,202.568
28,194.341 28,194.341
29,189.916 29,189.916
30,154.228 30,154.228
31,168.922 31,168.922
32,173.623 32,173.623
33,125.431 33,125.431
34,162.154 34,162.154
35,149.865 35,149.865
36,150.088 36,150.088
37,146.085 37,146.085
38,137.182 38,137.182
39,138.686 39,138.686
40,136.302 40,136.302
41,132.707 41,132.707
42,100.928 42,100.928
43,126.414 43,126.414
44,125.271 44,125.271
45,117.839 45,117.839
46,89.494 46,89.494
47,116.939 47,116.939
48,112.517 48,112.517
49,111.369 49,111.369
50,108.568 50,108.568
1 0 4090.616
2 1 2211.62
3 2 1165.45
4 3 849.39
5 4 828.635
6 5 741.537
7 6 632.721
8 7 624.772
9 8 529.234
10 9 469.688
11 10 336.33
12 11 427.783
13 12 400.662
14 13 335.086
15 14 342.042
16 15 307.283
17 16 239.694
18 17 295.163
19 18 285.787
20 19 254.402
21 20 250.553
22 21 227.146
23 22 238.747
24 23 234.718
25 24 210.484
26 25 210.697
27 26 205.943
28 27 202.568
29 28 194.341
30 29 189.916
31 30 154.228
32 31 168.922
33 32 173.623
34 33 125.431
35 34 162.154
36 35 149.865
37 36 150.088
38 37 146.085
39 38 137.182
40 39 138.686
41 40 136.302
42 41 132.707
43 42 100.928
44 43 126.414
45 44 125.271
46 45 117.839
47 46 89.494
48 47 116.939
49 48 112.517
50 49 111.369
51 50 108.568

View File

@@ -1,51 +1,51 @@
0,3789.988 0,3789.988
1,2069.487 1,2069.487
2,1484.554 2,1484.554
3,956.972 3,956.972
4,1052.873 4,1052.873
5,739.658 5,739.658
6,534.722 6,534.722
7,638.524 7,638.524
8,573.833 8,573.833
9,531.658 9,531.658
10,476.167 10,476.167
11,443.746 11,443.746
12,406.027 12,406.027
13,385.739 13,385.739
14,341.563 14,341.563
15,318.699 15,318.699
16,303.722 16,303.722
17,284.924 17,284.924
18,284.336 18,284.336
19,267.32 19,267.32
20,202.74 20,202.74
21,243.849 21,243.849
22,226.082 22,226.082
23,214.348 23,214.348
24,216.8 24,216.8
25,188.98 25,188.98
26,158.68 26,158.68
27,166.556 27,166.556
28,148.287 28,148.287
29,149.681 29,149.681
30,177.043 30,177.043
31,175.321 31,175.321
32,165.312 32,165.312
33,166.943 33,166.943
34,159.026 34,159.026
35,156.759 35,156.759
36,150.216 36,150.216
37,144.932 37,144.932
38,146.088 38,146.088
39,135.897 39,135.897
40,136.99 40,136.99
41,128.557 41,128.557
42,100.307 42,100.307
43,103.249 43,103.249
44,123.49 44,123.49
45,120.39 45,120.39
46,118.055 46,118.055
47,115.0 47,115.0
48,112.593 48,112.593
49,109.55 49,109.55
50,109.512 50,109.512
1 0 3789.988
2 1 2069.487
3 2 1484.554
4 3 956.972
5 4 1052.873
6 5 739.658
7 6 534.722
8 7 638.524
9 8 573.833
10 9 531.658
11 10 476.167
12 11 443.746
13 12 406.027
14 13 385.739
15 14 341.563
16 15 318.699
17 16 303.722
18 17 284.924
19 18 284.336
20 19 267.32
21 20 202.74
22 21 243.849
23 22 226.082
24 23 214.348
25 24 216.8
26 25 188.98
27 26 158.68
28 27 166.556
29 28 148.287
30 29 149.681
31 30 177.043
32 31 175.321
33 32 165.312
34 33 166.943
35 34 159.026
36 35 156.759
37 36 150.216
38 37 144.932
39 38 146.088
40 39 135.897
41 40 136.99
42 41 128.557
43 42 100.307
44 43 103.249
45 44 123.49
46 45 120.39
47 46 118.055
48 47 115.0
49 48 112.593
50 49 109.55
51 50 109.512

View File

@@ -1,51 +1,51 @@
0,4216.05 0,4216.05
1,4239.598 1,4239.598
2,2418.527 2,2418.527
3,2227.8 3,2227.8
4,2045.351 4,2045.351
5,2066.161 5,2066.161
6,2214.416 6,2214.416
7,2052.845 7,2052.845
8,2195.199 8,2195.199
9,2186.867 9,2186.867
10,2147.534 10,2147.534
11,2186.652 11,2186.652
12,2178.036 12,2178.036
13,2182.151 13,2182.151
14,2185.324 14,2185.324
15,1812.911 15,1812.911
16,2144.689 16,2144.689
17,2163.525 17,2163.525
18,2073.89 18,2073.89
19,2071.682 19,2071.682
20,2153.502 20,2153.502
21,2144.04 21,2144.04
22,2118.517 22,2118.517
23,2141.19 23,2141.19
24,2167.103 24,2167.103
25,2168.631 25,2168.631
26,2165.555 26,2165.555
27,2158.424 27,2158.424
28,2188.376 28,2188.376
29,2165.311 29,2165.311
30,2168.158 30,2168.158
31,2108.045 31,2108.045
32,2121.414 32,2121.414
33,2022.533 33,2022.533
34,1888.759 34,1888.759
35,2022.837 35,2022.837
36,2015.042 36,2015.042
37,1920.401 37,1920.401
38,2005.037 38,2005.037
39,2028.856 39,2028.856
40,2010.43 40,2010.43
41,1522.342 41,1522.342
42,1525.635 42,1525.635
43,1912.05 43,1912.05
44,1920.256 44,1920.256
45,1753.645 45,1753.645
46,1476.977 46,1476.977
47,1888.645 47,1888.645
48,1949.103 48,1949.103
49,1684.633 49,1684.633
50,1493.935 50,1493.935
1 0 4216.05
2 1 4239.598
3 2 2418.527
4 3 2227.8
5 4 2045.351
6 5 2066.161
7 6 2214.416
8 7 2052.845
9 8 2195.199
10 9 2186.867
11 10 2147.534
12 11 2186.652
13 12 2178.036
14 13 2182.151
15 14 2185.324
16 15 1812.911
17 16 2144.689
18 17 2163.525
19 18 2073.89
20 19 2071.682
21 20 2153.502
22 21 2144.04
23 22 2118.517
24 23 2141.19
25 24 2167.103
26 25 2168.631
27 26 2165.555
28 27 2158.424
29 28 2188.376
30 29 2165.311
31 30 2168.158
32 31 2108.045
33 32 2121.414
34 33 2022.533
35 34 1888.759
36 35 2022.837
37 36 2015.042
38 37 1920.401
39 38 2005.037
40 39 2028.856
41 40 2010.43
42 41 1522.342
43 42 1525.635
44 43 1912.05
45 44 1920.256
46 45 1753.645
47 46 1476.977
48 47 1888.645
49 48 1949.103
50 49 1684.633
51 50 1493.935

View File

@@ -1,51 +1,51 @@
0,4203.31 0,4203.31
1,4283.392 1,4283.392
2,2383.415 2,2383.415
3,2419.701 3,2419.701
4,2038.823 4,2038.823
5,2038.0 5,2038.0
6,2160.869 6,2160.869
7,2192.641 7,2192.641
8,2216.766 8,2216.766
9,2762.56 9,2762.56
10,2160.398 10,2160.398
11,2147.886 11,2147.886
12,2146.47 12,2146.47
13,2158.101 13,2158.101
14,2154.025 14,2154.025
15,1997.694 15,1997.694
16,2028.288 16,2028.288
17,2005.373 17,2005.373
18,2153.945 18,2153.945
19,2190.799 19,2190.799
20,2169.302 20,2169.302
21,2139.842 21,2139.842
22,2155.307 22,2155.307
23,2152.223 23,2152.223
24,2124.155 24,2124.155
25,2103.135 25,2103.135
26,2148.053 26,2148.053
27,2163.366 27,2163.366
28,2122.339 28,2122.339
29,2064.701 29,2064.701
30,2134.748 30,2134.748
31,1632.533 31,1632.533
32,2082.309 32,2082.309
33,1878.795 33,1878.795
34,2009.28 34,2009.28
35,1987.424 35,1987.424
36,1748.364 36,1748.364
37,1725.66 37,1725.66
38,1967.877 38,1967.877
39,1854.637 39,1854.637
40,1903.963 40,1903.963
41,1987.138 41,1987.138
42,1532.547 42,1532.547
43,1569.27 43,1569.27
44,1535.941 44,1535.941
45,1941.715 45,1941.715
46,2014.504 46,2014.504
47,2005.794 47,2005.794
48,2022.972 48,2022.972
49,1740.836 49,1740.836
50,1726.444 50,1726.444
1 0 4203.31
2 1 4283.392
3 2 2383.415
4 3 2419.701
5 4 2038.823
6 5 2038.0
7 6 2160.869
8 7 2192.641
9 8 2216.766
10 9 2762.56
11 10 2160.398
12 11 2147.886
13 12 2146.47
14 13 2158.101
15 14 2154.025
16 15 1997.694
17 16 2028.288
18 17 2005.373
19 18 2153.945
20 19 2190.799
21 20 2169.302
22 21 2139.842
23 22 2155.307
24 23 2152.223
25 24 2124.155
26 25 2103.135
27 26 2148.053
28 27 2163.366
29 28 2122.339
30 29 2064.701
31 30 2134.748
32 31 1632.533
33 32 2082.309
34 33 1878.795
35 34 2009.28
36 35 1987.424
37 36 1748.364
38 37 1725.66
39 38 1967.877
40 39 1854.637
41 40 1903.963
42 41 1987.138
43 42 1532.547
44 43 1569.27
45 44 1535.941
46 45 1941.715
47 46 2014.504
48 47 2005.794
49 48 2022.972
50 49 1740.836
51 50 1726.444

View File

@@ -1,51 +1,51 @@
0,710.619 0,710.619
1,887.877 1,887.877
2,981.431 2,981.431
3,1081.412 3,1081.412
4,1038.514 4,1038.514
5,1029.805 5,1029.805
6,928.317 6,928.317
7,1130.938 7,1130.938
8,1165.42 8,1165.42
9,925.632 9,925.632
10,949.483 10,949.483
11,1021.973 11,1021.973
12,903.878 12,903.878
13,1001.53 13,1001.53
14,895.351 14,895.351
15,1026.722 15,1026.722
16,634.727 16,634.727
17,744.758 17,744.758
18,978.59 18,978.59
19,962.375 19,962.375
20,997.471 20,997.471
21,929.785 21,929.785
22,1200.83 22,1200.83
23,1257.741 23,1257.741
24,772.729 24,772.729
25,683.913 25,683.913
26,1188.17 26,1188.17
27,919.961 27,919.961
28,922.225 28,922.225
29,1066.286 29,1066.286
30,979.399 30,979.399
31,978.917 31,978.917
32,988.415 32,988.415
33,1061.523 33,1061.523
34,942.85 34,942.85
35,1045.949 35,1045.949
36,883.941 36,883.941
37,958.41 37,958.41
38,989.523 38,989.523
39,1001.121 39,1001.121
40,1080.079 40,1080.079
41,1151.938 41,1151.938
42,1221.644 42,1221.644
43,991.855 43,991.855
44,1088.344 44,1088.344
45,973.641 45,973.641
46,952.35 46,952.35
47,1089.644 47,1089.644
48,939.615 48,939.615
49,1258.419 49,1258.419
50,949.414 50,949.414
1 0 710.619
2 1 887.877
3 2 981.431
4 3 1081.412
5 4 1038.514
6 5 1029.805
7 6 928.317
8 7 1130.938
9 8 1165.42
10 9 925.632
11 10 949.483
12 11 1021.973
13 12 903.878
14 13 1001.53
15 14 895.351
16 15 1026.722
17 16 634.727
18 17 744.758
19 18 978.59
20 19 962.375
21 20 997.471
22 21 929.785
23 22 1200.83
24 23 1257.741
25 24 772.729
26 25 683.913
27 26 1188.17
28 27 919.961
29 28 922.225
30 29 1066.286
31 30 979.399
32 31 978.917
33 32 988.415
34 33 1061.523
35 34 942.85
36 35 1045.949
37 36 883.941
38 37 958.41
39 38 989.523
40 39 1001.121
41 40 1080.079
42 41 1151.938
43 42 1221.644
44 43 991.855
45 44 1088.344
46 45 973.641
47 46 952.35
48 47 1089.644
49 48 939.615
50 49 1258.419
51 50 949.414

View File

@@ -1,51 +1,51 @@
0,3245.763 0,3245.763
1,3283.646 1,3283.646
2,3741.157 2,3741.157
3,3691.206 3,3691.206
4,3365.134 4,3365.134
5,3691.457 5,3691.457
6,3354.807 6,3354.807
7,3526.728 7,3526.728
8,3252.62 8,3252.62
9,3551.086 9,3551.086
10,3561.506 10,3561.506
11,3525.577 11,3525.577
12,2776.064 12,2776.064
13,3541.86 13,3541.86
14,3501.34 14,3501.34
15,3692.092 15,3692.092
16,3637.166 16,3637.166
17,3617.031 17,3617.031
18,3700.092 18,3700.092
19,3176.831 19,3176.831
20,3368.038 20,3368.038
21,3716.577 21,3716.577
22,3452.917 22,3452.917
23,3617.604 23,3617.604
24,3651.796 24,3651.796
25,3552.053 25,3552.053
26,3843.18 26,3843.18
27,3720.406 27,3720.406
28,3431.1 28,3431.1
29,3578.973 29,3578.973
30,3561.994 30,3561.994
31,3524.566 31,3524.566
32,3567.537 32,3567.537
33,3626.767 33,3626.767
34,3498.361 34,3498.361
35,3621.396 35,3621.396
36,3297.839 36,3297.839
37,3541.207 37,3541.207
38,3560.364 38,3560.364
39,3589.746 39,3589.746
40,3686.673 40,3686.673
41,3463.811 41,3463.811
42,3428.408 42,3428.408
43,3753.139 43,3753.139
44,3368.89 44,3368.89
45,3324.876 45,3324.876
46,3614.895 46,3614.895
47,3245.942 47,3245.942
48,3257.925 48,3257.925
49,3200.585 49,3200.585
50,3321.55 50,3321.55
1 0 3245.763
2 1 3283.646
3 2 3741.157
4 3 3691.206
5 4 3365.134
6 5 3691.457
7 6 3354.807
8 7 3526.728
9 8 3252.62
10 9 3551.086
11 10 3561.506
12 11 3525.577
13 12 2776.064
14 13 3541.86
15 14 3501.34
16 15 3692.092
17 16 3637.166
18 17 3617.031
19 18 3700.092
20 19 3176.831
21 20 3368.038
22 21 3716.577
23 22 3452.917
24 23 3617.604
25 24 3651.796
26 25 3552.053
27 26 3843.18
28 27 3720.406
29 28 3431.1
30 29 3578.973
31 30 3561.994
32 31 3524.566
33 32 3567.537
34 33 3626.767
35 34 3498.361
36 35 3621.396
37 36 3297.839
38 37 3541.207
39 38 3560.364
40 39 3589.746
41 40 3686.673
42 41 3463.811
43 42 3428.408
44 43 3753.139
45 44 3368.89
46 45 3324.876
47 46 3614.895
48 47 3245.942
49 48 3257.925
50 49 3200.585
51 50 3321.55

View File

@@ -1,51 +1,51 @@
0,1790.382 0,1790.382
1,1933.881 1,1933.881
2,1941.564 2,1941.564
3,1926.518 3,1926.518
4,1945.295 4,1945.295
5,1734.462 5,1734.462
6,2009.994 6,2009.994
7,2007.538 7,2007.538
8,2004.825 8,2004.825
9,1848.551 9,1848.551
10,1836.558 10,1836.558
11,1977.19 11,1977.19
12,1987.207 12,1987.207
13,2007.422 13,2007.422
14,1994.914 14,1994.914
15,1982.997 15,1982.997
16,1955.828 16,1955.828
17,1705.883 17,1705.883
18,1983.501 18,1983.501
19,1951.311 19,1951.311
20,1921.772 20,1921.772
21,1956.908 21,1956.908
22,1948.865 22,1948.865
23,1929.387 23,1929.387
24,1814.539 24,1814.539
25,2084.284 25,2084.284
26,1830.901 26,1830.901
27,1946.713 27,1946.713
28,1958.238 28,1958.238
29,1906.573 29,1906.573
30,1895.341 30,1895.341
31,1986.09 31,1986.09
32,1943.785 32,1943.785
33,1879.917 33,1879.917
34,1946.029 34,1946.029
35,1858.958 35,1858.958
36,2009.44 36,2009.44
37,1876.749 37,1876.749
38,1967.254 38,1967.254
39,1968.595 39,1968.595
40,1846.438 40,1846.438
41,1955.897 41,1955.897
42,1986.446 42,1986.446
43,1965.143 43,1965.143
44,1963.016 44,1963.016
45,1890.88 45,1890.88
46,1998.801 46,1998.801
47,1682.048 47,1682.048
48,2023.688 48,2023.688
49,1982.952 49,1982.952
50,1993.641 50,1993.641
1 0 1790.382
2 1 1933.881
3 2 1941.564
4 3 1926.518
5 4 1945.295
6 5 1734.462
7 6 2009.994
8 7 2007.538
9 8 2004.825
10 9 1848.551
11 10 1836.558
12 11 1977.19
13 12 1987.207
14 13 2007.422
15 14 1994.914
16 15 1982.997
17 16 1955.828
18 17 1705.883
19 18 1983.501
20 19 1951.311
21 20 1921.772
22 21 1956.908
23 22 1948.865
24 23 1929.387
25 24 1814.539
26 25 2084.284
27 26 1830.901
28 27 1946.713
29 28 1958.238
30 29 1906.573
31 30 1895.341
32 31 1986.09
33 32 1943.785
34 33 1879.917
35 34 1946.029
36 35 1858.958
37 36 2009.44
38 37 1876.749
39 38 1967.254
40 39 1968.595
41 40 1846.438
42 41 1955.897
43 42 1986.446
44 43 1965.143
45 44 1963.016
46 45 1890.88
47 46 1998.801
48 47 1682.048
49 48 2023.688
50 49 1982.952
51 50 1993.641

View File

@@ -1,51 +1,51 @@
0,4007.679 0,4007.679
1,3963.986 1,3963.986
2,4222.243 2,4222.243
3,3640.707 3,3640.707
4,4388.553 4,4388.553
5,3636.047 5,3636.047
6,3644.611 6,3644.611
7,3547.39 7,3547.39
8,3412.162 8,3412.162
9,3632.367 9,3632.367
10,3536.655 10,3536.655
11,3820.019 11,3820.019
12,3677.177 12,3677.177
13,3366.323 13,3366.323
14,3353.031 14,3353.031
15,3392.423 15,3392.423
16,3330.368 16,3330.368
17,3363.272 17,3363.272
18,4027.34 18,4027.34
19,3467.982 19,3467.982
20,3607.754 20,3607.754
21,3767.614 21,3767.614
22,3340.544 22,3340.544
23,4086.612 23,4086.612
24,3784.164 24,3784.164
25,3496.518 25,3496.518
26,3543.808 26,3543.808
27,3453.934 27,3453.934
28,3546.188 28,3546.188
29,3458.804 29,3458.804
30,3728.609 30,3728.609
31,3697.624 31,3697.624
32,3698.191 32,3698.191
33,3673.973 33,3673.973
34,3690.046 34,3690.046
35,3663.799 35,3663.799
36,3540.004 36,3540.004
37,3857.604 37,3857.604
38,3426.215 38,3426.215
39,3704.176 39,3704.176
40,3796.133 40,3796.133
41,3604.623 41,3604.623
42,3650.508 42,3650.508
43,3501.861 43,3501.861
44,3685.992 44,3685.992
45,3623.404 45,3623.404
46,3728.601 46,3728.601
47,3844.994 47,3844.994
48,3820.046 48,3820.046
49,3680.976 49,3680.976
50,3797.432 50,3797.432
1 0 4007.679
2 1 3963.986
3 2 4222.243
4 3 3640.707
5 4 4388.553
6 5 3636.047
7 6 3644.611
8 7 3547.39
9 8 3412.162
10 9 3632.367
11 10 3536.655
12 11 3820.019
13 12 3677.177
14 13 3366.323
15 14 3353.031
16 15 3392.423
17 16 3330.368
18 17 3363.272
19 18 4027.34
20 19 3467.982
21 20 3607.754
22 21 3767.614
23 22 3340.544
24 23 4086.612
25 24 3784.164
26 25 3496.518
27 26 3543.808
28 27 3453.934
29 28 3546.188
30 29 3458.804
31 30 3728.609
32 31 3697.624
33 32 3698.191
34 33 3673.973
35 34 3690.046
36 35 3663.799
37 36 3540.004
38 37 3857.604
39 38 3426.215
40 39 3704.176
41 40 3796.133
42 41 3604.623
43 42 3650.508
44 43 3501.861
45 44 3685.992
46 45 3623.404
47 46 3728.601
48 47 3844.994
49 48 3820.046
50 49 3680.976
51 50 3797.432

View File

@@ -1,101 +1,101 @@
No filters,test data No filters,test data
1600.27,1772.897 1600.27,1772.897
1486.257,1455.93 1486.257,1455.93
1534.667,1403.539 1534.667,1403.539
1244.374,1665.846 1244.374,1665.846
1569.867,1627.449 1569.867,1627.449
1522.719,1084.153 1522.719,1084.153
1391.244,1259.783 1391.244,1259.783
1528.465,1282.901 1528.465,1282.901
1310.989,1275.515 1310.989,1275.515
1675.138,1074.39 1675.138,1074.39
1393.644,1359.139 1393.644,1359.139
1639.889,1162.937 1639.889,1162.937
1658.168,1239.767 1658.168,1239.767
1477.156,1308.195 1477.156,1308.195
1224.386,1298.007 1224.386,1298.007
1420.7,1087.031 1420.7,1087.031
1353.746,1090.502 1353.746,1090.502
1759.778,1179.381 1759.778,1179.381
1414.33,1222.86 1414.33,1222.86
1475.981,1295.207 1475.981,1295.207
1375.197,1327.8 1375.197,1327.8
1265.015,1189.121 1265.015,1189.121
1335.179,1594.98 1335.179,1594.98
1191.896,1271.873 1191.896,1271.873
1596.418,1100.372 1596.418,1100.372
1433.755,1147.945 1433.755,1147.945
1213.187,1312.989 1213.187,1312.989
1157.99,1153.825 1157.99,1153.825
1322.314,1184.481 1322.314,1184.481
1262.974,1271.012 1262.974,1271.012
1266.223,1350.519 1266.223,1350.519
1192.275,1199.142 1192.275,1199.142
1296.164,1189.432 1296.164,1189.432
1245.501,1185.107 1245.501,1185.107
1293.076,1374.689 1293.076,1374.689
1260.554,1384.055 1260.554,1384.055
1219.219,1420.395 1219.219,1420.395
1132.234,1099.141 1132.234,1099.141
1129.541,1101.805 1129.541,1101.805
1273.171,1210.564 1273.171,1210.564
1269.415,1184.094 1269.415,1184.094
1370.586,1321.974 1370.586,1321.974
1303.694,1317.074 1303.694,1317.074
1413.705,1380.092 1413.705,1380.092
1324.827,1142.097 1324.827,1142.097
1124.399,1548.557 1124.399,1548.557
1137.381,1029.353 1137.381,1029.353
1419.146,1326.829 1419.146,1326.829
1342.397,1270.316 1342.397,1270.316
1546.898,1258.933 1546.898,1258.933
1268.918,1062.23 1268.918,1062.23
1239.877,1234.887 1239.877,1234.887
1474.269,1181.184 1474.269,1181.184
1289.763,1139.728 1289.763,1139.728
1387.416,1125.734 1387.416,1125.734
1128.784,1278.381 1128.784,1278.381
1519.4,1243.597 1519.4,1243.597
1343.003,1153.18 1343.003,1153.18
1547.543,1117.816 1547.543,1117.816
1582.958,1594.145 1582.958,1594.145
1618.213,1358.087 1618.213,1358.087
1449.399,1295.487 1449.399,1295.487
1373.062,1174.153 1373.062,1174.153
1211.207,1346.833 1211.207,1346.833
1066.275,1417.633 1066.275,1417.633
1203.659,1131.727 1203.659,1131.727
1129.005,1351.061 1129.005,1351.061
1200.245,1615.952 1200.245,1615.952
1232.596,1250.436 1232.596,1250.436
1262.319,1563.46 1262.319,1563.46
1127.022,1651.89 1127.022,1651.89
1736.368,1561.661 1736.368,1561.661
1310.858,1459.713 1310.858,1459.713
1351.455,1608.494 1351.455,1608.494
1156.124,1440.379 1156.124,1440.379
1220.053,1267.708 1220.053,1267.708
1171.428,1300.284 1171.428,1300.284
1149.242,1087.453 1149.242,1087.453
1213.915,1081.207 1213.915,1081.207
1092.869,1402.761 1092.869,1402.761
1243.623,1321.907 1243.623,1321.907
1216.257,1217.721 1216.257,1217.721
1221.354,1263.695 1221.354,1263.695
1242.771,1241.684 1242.771,1241.684
1427.276,1322.01 1427.276,1322.01
1328.502,1346.21 1328.502,1346.21
1275.719,1269.909 1275.719,1269.909
1372.075,1451.069 1372.075,1451.069
1486.541,1532.56 1486.541,1532.56
1577.036,1539.804 1577.036,1539.804
1628.025,1372.806 1628.025,1372.806
1415.623,1239.201 1415.623,1239.201
1198.632,1095.849 1198.632,1095.849
1170.341,1255.875 1170.341,1255.875
1214.99,1424.292 1214.99,1424.292
1356.431,1135.588 1356.431,1135.588
1817.822,1212.386 1817.822,1212.386
1745.199,1170.863 1745.199,1170.863
1779.083,1145.458 1779.083,1145.458
1544.934,1076.386 1544.934,1076.386
1 No filters test data
2 1600.27 1772.897
3 1486.257 1455.93
4 1534.667 1403.539
5 1244.374 1665.846
6 1569.867 1627.449
7 1522.719 1084.153
8 1391.244 1259.783
9 1528.465 1282.901
10 1310.989 1275.515
11 1675.138 1074.39
12 1393.644 1359.139
13 1639.889 1162.937
14 1658.168 1239.767
15 1477.156 1308.195
16 1224.386 1298.007
17 1420.7 1087.031
18 1353.746 1090.502
19 1759.778 1179.381
20 1414.33 1222.86
21 1475.981 1295.207
22 1375.197 1327.8
23 1265.015 1189.121
24 1335.179 1594.98
25 1191.896 1271.873
26 1596.418 1100.372
27 1433.755 1147.945
28 1213.187 1312.989
29 1157.99 1153.825
30 1322.314 1184.481
31 1262.974 1271.012
32 1266.223 1350.519
33 1192.275 1199.142
34 1296.164 1189.432
35 1245.501 1185.107
36 1293.076 1374.689
37 1260.554 1384.055
38 1219.219 1420.395
39 1132.234 1099.141
40 1129.541 1101.805
41 1273.171 1210.564
42 1269.415 1184.094
43 1370.586 1321.974
44 1303.694 1317.074
45 1413.705 1380.092
46 1324.827 1142.097
47 1124.399 1548.557
48 1137.381 1029.353
49 1419.146 1326.829
50 1342.397 1270.316
51 1546.898 1258.933
52 1268.918 1062.23
53 1239.877 1234.887
54 1474.269 1181.184
55 1289.763 1139.728
56 1387.416 1125.734
57 1128.784 1278.381
58 1519.4 1243.597
59 1343.003 1153.18
60 1547.543 1117.816
61 1582.958 1594.145
62 1618.213 1358.087
63 1449.399 1295.487
64 1373.062 1174.153
65 1211.207 1346.833
66 1066.275 1417.633
67 1203.659 1131.727
68 1129.005 1351.061
69 1200.245 1615.952
70 1232.596 1250.436
71 1262.319 1563.46
72 1127.022 1651.89
73 1736.368 1561.661
74 1310.858 1459.713
75 1351.455 1608.494
76 1156.124 1440.379
77 1220.053 1267.708
78 1171.428 1300.284
79 1149.242 1087.453
80 1213.915 1081.207
81 1092.869 1402.761
82 1243.623 1321.907
83 1216.257 1217.721
84 1221.354 1263.695
85 1242.771 1241.684
86 1427.276 1322.01
87 1328.502 1346.21
88 1275.719 1269.909
89 1372.075 1451.069
90 1486.541 1532.56
91 1577.036 1539.804
92 1628.025 1372.806
93 1415.623 1239.201
94 1198.632 1095.849
95 1170.341 1255.875
96 1214.99 1424.292
97 1356.431 1135.588
98 1817.822 1212.386
99 1745.199 1170.863
100 1779.083 1145.458
101 1544.934 1076.386

View File

@@ -1,101 +1,101 @@
No filters,test data No filters,test data
2098.666,2118.781 2098.666,2118.781
2175.2,2086.957 2175.2,2086.957
2177.653,1795.287 2177.653,1795.287
1775.63,1745.066 1775.63,1745.066
1827.78,2038.921 1827.78,2038.921
1813.369,2179.81 1813.369,2179.81
1988.859,2176.883 1988.859,2176.883
1634.541,1704.071 1634.541,1704.071
1878.829,1869.999 1878.829,1869.999
1738.987,2024.959 1738.987,2024.959
1920.502,1477.726 1920.502,1477.726
1895.909,1732.832 1895.909,1732.832
1812.012,1850.978 1812.012,1850.978
1908.106,1902.953 1908.106,1902.953
2112.837,1726.547 2112.837,1726.547
1765.808,1710.915 1765.808,1710.915
1918.121,1900.619 1918.121,1900.619
1892.779,2054.93 1892.779,2054.93
1852.952,2113.928 1852.952,2113.928
1713.67,1770.379 1713.67,1770.379
1873.637,2011.518 1873.637,2011.518
1787.007,2104.061 1787.007,2104.061
1764.704,2134.151 1764.704,2134.151
2064.776,2073.226 2064.776,2073.226
1838.23,1762.436 1838.23,1762.436
1808.339,1792.41 1808.339,1792.41
1756.516,1706.501 1756.516,1706.501
1665.888,1610.771 1665.888,1610.771
1682.272,1650.033 1682.272,1650.033
1690.473,1563.995 1690.473,1563.995
1997.801,1955.53 1997.801,1955.53
1660.487,1669.25 1660.487,1669.25
2023.106,1727.046 2023.106,1727.046
1724.59,1686.137 1724.59,1686.137
1697.656,1627.136 1697.656,1627.136
1689.65,1571.13 1689.65,1571.13
1628.35,1699.239 1628.35,1699.239
1843.768,1825.739 1843.768,1825.739
1715.158,1573.695 1715.158,1573.695
1732.695,1875.656 1732.695,1875.656
1902.818,1968.505 1902.818,1968.505
1699.277,1919.737 1699.277,1919.737
1618.75,2015.258 1618.75,2015.258
1696.055,2014.261 1696.055,2014.261
1792.486,1606.754 1792.486,1606.754
1889.583,1625.965 1889.583,1625.965
1716.951,1572.049 1716.951,1572.049
1727.305,1649.502 1727.305,1649.502
1747.618,2099.787 1747.618,2099.787
1698.546,2153.363 1698.546,2153.363
1723.117,1637.074 1723.117,1637.074
1654.061,1721.968 1654.061,1721.968
1735.332,1587.906 1735.332,1587.906
1841.808,1565.797 1841.808,1565.797
2006.973,1665.615 2006.973,1665.615
1730.909,1883.505 1730.909,1883.505
1681.954,1553.826 1681.954,1553.826
1653.215,1849.824 1653.215,1849.824
2072.138,1990.474 2072.138,1990.474
1792.302,2176.718 1792.302,2176.718
1679.381,2128.083 1679.381,2128.083
1653.368,2078.013 1653.368,2078.013
1399.58,2065.031 1399.58,2065.031
1669.979,1815.553 1669.979,1815.553
1677.346,1870.055 1677.346,1870.055
1652.22,2010.441 1652.22,2010.441
1870.35,1687.893 1870.35,1687.893
1772.229,1857.193 1772.229,1857.193
1743.552,1813.027 1743.552,1813.027
1685.312,1466.505 1685.312,1466.505
1863.269,1813.398 1863.269,1813.398
1694.335,1889.661 1694.335,1889.661
1739.016,1740.381 1739.016,1740.381
1764.462,1752.725 1764.462,1752.725
1702.134,2069.289 1702.134,2069.289
1955.771,2176.617 1955.771,2176.617
2046.117,2137.499 2046.117,2137.499
1766.64,2177.955 1766.64,2177.955
1733.26,2148.497 1733.26,2148.497
1834.827,2161.573 1834.827,2161.573
2087.089,2119.311 2087.089,2119.311
2154.753,1679.596 2154.753,1679.596
2073.729,1912.012 2073.729,1912.012
2082.37,1841.045 2082.37,1841.045
2160.86,1813.257 2160.86,1813.257
1678.515,1894.864 1678.515,1894.864
1758.394,1884.985 1758.394,1884.985
1673.919,1732.373 1673.919,1732.373
1666.474,1737.66 1666.474,1737.66
1679.444,1463.082 1679.444,1463.082
1684.006,2002.343 1684.006,2002.343
1737.287,2026.394 1737.287,2026.394
1811.305,2084.689 1811.305,2084.689
2127.121,2117.391 2127.121,2117.391
2139.884,1984.606 2139.884,1984.606
1677.256,1770.76 1677.256,1770.76
1698.544,1833.011 1698.544,1833.011
1905.446,1734.777 1905.446,1734.777
1913.257,1688.401 1913.257,1688.401
2063.73,1667.27 2063.73,1667.27
1 No filters test data
2 2098.666 2118.781
3 2175.2 2086.957
4 2177.653 1795.287
5 1775.63 1745.066
6 1827.78 2038.921
7 1813.369 2179.81
8 1988.859 2176.883
9 1634.541 1704.071
10 1878.829 1869.999
11 1738.987 2024.959
12 1920.502 1477.726
13 1895.909 1732.832
14 1812.012 1850.978
15 1908.106 1902.953
16 2112.837 1726.547
17 1765.808 1710.915
18 1918.121 1900.619
19 1892.779 2054.93
20 1852.952 2113.928
21 1713.67 1770.379
22 1873.637 2011.518
23 1787.007 2104.061
24 1764.704 2134.151
25 2064.776 2073.226
26 1838.23 1762.436
27 1808.339 1792.41
28 1756.516 1706.501
29 1665.888 1610.771
30 1682.272 1650.033
31 1690.473 1563.995
32 1997.801 1955.53
33 1660.487 1669.25
34 2023.106 1727.046
35 1724.59 1686.137
36 1697.656 1627.136
37 1689.65 1571.13
38 1628.35 1699.239
39 1843.768 1825.739
40 1715.158 1573.695
41 1732.695 1875.656
42 1902.818 1968.505
43 1699.277 1919.737
44 1618.75 2015.258
45 1696.055 2014.261
46 1792.486 1606.754
47 1889.583 1625.965
48 1716.951 1572.049
49 1727.305 1649.502
50 1747.618 2099.787
51 1698.546 2153.363
52 1723.117 1637.074
53 1654.061 1721.968
54 1735.332 1587.906
55 1841.808 1565.797
56 2006.973 1665.615
57 1730.909 1883.505
58 1681.954 1553.826
59 1653.215 1849.824
60 2072.138 1990.474
61 1792.302 2176.718
62 1679.381 2128.083
63 1653.368 2078.013
64 1399.58 2065.031
65 1669.979 1815.553
66 1677.346 1870.055
67 1652.22 2010.441
68 1870.35 1687.893
69 1772.229 1857.193
70 1743.552 1813.027
71 1685.312 1466.505
72 1863.269 1813.398
73 1694.335 1889.661
74 1739.016 1740.381
75 1764.462 1752.725
76 1702.134 2069.289
77 1955.771 2176.617
78 2046.117 2137.499
79 1766.64 2177.955
80 1733.26 2148.497
81 1834.827 2161.573
82 2087.089 2119.311
83 2154.753 1679.596
84 2073.729 1912.012
85 2082.37 1841.045
86 2160.86 1813.257
87 1678.515 1894.864
88 1758.394 1884.985
89 1673.919 1732.373
90 1666.474 1737.66
91 1679.444 1463.082
92 1684.006 2002.343
93 1737.287 2026.394
94 1811.305 2084.689
95 2127.121 2117.391
96 2139.884 1984.606
97 1677.256 1770.76
98 1698.544 1833.011
99 1905.446 1734.777
100 1913.257 1688.401
101 2063.73 1667.27

View File

@@ -1,101 +1,101 @@
No filters,test data No filters,test data
3841.832,3177.356 3841.832,3177.356
3369.899,3819.926 3369.899,3819.926
3884.689,2843.759 3884.689,2843.759
3391.267,3106.399 3391.267,3106.399
3740.054,2899.246 3740.054,2899.246
3754.086,3254.525 3754.086,3254.525
3284.178,3180.96 3284.178,3180.96
3293.044,3356.928 3293.044,3356.928
3653.05,2925.883 3653.05,2925.883
3830.609,2784.715 3830.609,2784.715
3691.078,3283.715 3691.078,3283.715
3551.286,3437.899 3551.286,3437.899
3651.296,2759.088 3651.296,2759.088
3726.295,3289.184 3726.295,3289.184
3860.353,3067.069 3860.353,3067.069
3910.997,3764.354 3910.997,3764.354
3775.794,3182.171 3775.794,3182.171
3824.719,3376.774 3824.719,3376.774
3245.109,2954.582 3245.109,2954.582
3705.489,4101.548 3705.489,4101.548
3484.114,3155.55 3484.114,3155.55
3742.727,3153.767 3742.727,3153.767
3964.472,3624.241 3964.472,3624.241
3747.219,2787.965 3747.219,2787.965
3746.575,3518.095 3746.575,3518.095
3903.7,2942.676 3903.7,2942.676
3888.772,3222.041 3888.772,3222.041
3854.913,2479.502 3854.913,2479.502
3716.801,2876.082 3716.801,2876.082
3919.146,2748.543 3919.146,2748.543
3908.195,2742.45 3908.195,2742.45
3894.436,3135.703 3894.436,3135.703
3615.381,3411.222 3615.381,3411.222
3807.51,3525.049 3807.51,3525.049
3197.936,3515.207 3197.936,3515.207
3817.654,3505.676 3817.654,3505.676
3604.482,3749.862 3604.482,3749.862
4054.217,3389.18 4054.217,3389.18
4064.973,3110.13 4064.973,3110.13
3828.174,3994.395 3828.174,3994.395
3464.949,3706.928 3464.949,3706.928
3458.833,3818.998 3458.833,3818.998
3447.594,3354.733 3447.594,3354.733
3148.49,2938.606 3148.49,2938.606
3403.617,3000.615 3403.617,3000.615
3619.143,3712.188 3619.143,3712.188
3676.835,3294.72 3676.835,3294.72
4020.2,3668.025 4020.2,3668.025
3365.03,3288.992 3365.03,3288.992
3395.001,3047.487 3395.001,3047.487
3444.301,3644.15 3444.301,3644.15
3258.341,3412.968 3258.341,3412.968
3640.787,3028.915 3640.787,3028.915
3523.975,2984.702 3523.975,2984.702
3661.891,3124.492 3661.891,3124.492
3802.303,3098.351 3802.303,3098.351
3774.646,3486.505 3774.646,3486.505
3622.705,1967.98 3622.705,1967.98
3508.677,2629.166 3508.677,2629.166
3566.014,2717.307 3566.014,2717.307
3849.619,1697.053 3849.619,1697.053
3315.839,1708.413 3315.839,1708.413
3423.282,2104.829 3423.282,2104.829
3750.536,2822.277 3750.536,2822.277
3554.167,2610.241 3554.167,2610.241
3826.747,3645.146 3826.747,3645.146
3892.643,2795.429 3892.643,2795.429
3832.114,2572.367 3832.114,2572.367
3497.325,3586.324 3497.325,3586.324
3348.139,3108.224 3348.139,3108.224
3317.933,2944.826 3317.933,2944.826
3605.83,2890.459 3605.83,2890.459
3539.072,3132.536 3539.072,3132.536
3121.903,3343.355 3121.903,3343.355
2942.032,3478.153 2942.032,3478.153
3445.076,3762.927 3445.076,3762.927
3100.771,3377.621 3100.771,3377.621
3189.105,3326.58 3189.105,3326.58
3281.825,3443.852 3281.825,3443.852
2678.243,3830.363 2678.243,3830.363
2955.651,2863.628 2955.651,2863.628
2696.034,3640.54 2696.034,3640.54
3370.494,3203.94 3370.494,3203.94
3300.628,3755.641 3300.628,3755.641
3488.021,3931.192 3488.021,3931.192
3330.963,2780.609 3330.963,2780.609
3154.885,2986.501 3154.885,2986.501
3375.716,3359.562 3375.716,3359.562
2841.549,3077.406 2841.549,3077.406
3404.81,3385.657 3404.81,3385.657
3757.787,3352.594 3757.787,3352.594
3717.258,3264.236 3717.258,3264.236
3353.01,3659.337 3353.01,3659.337
3190.808,3732.121 3190.808,3732.121
3165.985,3380.969 3165.985,3380.969
3797.661,3264.325 3797.661,3264.325
3347.68,3711.328 3347.68,3711.328
3604.306,3454.656 3604.306,3454.656
3615.091,3547.976 3615.091,3547.976
3291.287,3115.255 3291.287,3115.255
1 No filters test data
2 3841.832 3177.356
3 3369.899 3819.926
4 3884.689 2843.759
5 3391.267 3106.399
6 3740.054 2899.246
7 3754.086 3254.525
8 3284.178 3180.96
9 3293.044 3356.928
10 3653.05 2925.883
11 3830.609 2784.715
12 3691.078 3283.715
13 3551.286 3437.899
14 3651.296 2759.088
15 3726.295 3289.184
16 3860.353 3067.069
17 3910.997 3764.354
18 3775.794 3182.171
19 3824.719 3376.774
20 3245.109 2954.582
21 3705.489 4101.548
22 3484.114 3155.55
23 3742.727 3153.767
24 3964.472 3624.241
25 3747.219 2787.965
26 3746.575 3518.095
27 3903.7 2942.676
28 3888.772 3222.041
29 3854.913 2479.502
30 3716.801 2876.082
31 3919.146 2748.543
32 3908.195 2742.45
33 3894.436 3135.703
34 3615.381 3411.222
35 3807.51 3525.049
36 3197.936 3515.207
37 3817.654 3505.676
38 3604.482 3749.862
39 4054.217 3389.18
40 4064.973 3110.13
41 3828.174 3994.395
42 3464.949 3706.928
43 3458.833 3818.998
44 3447.594 3354.733
45 3148.49 2938.606
46 3403.617 3000.615
47 3619.143 3712.188
48 3676.835 3294.72
49 4020.2 3668.025
50 3365.03 3288.992
51 3395.001 3047.487
52 3444.301 3644.15
53 3258.341 3412.968
54 3640.787 3028.915
55 3523.975 2984.702
56 3661.891 3124.492
57 3802.303 3098.351
58 3774.646 3486.505
59 3622.705 1967.98
60 3508.677 2629.166
61 3566.014 2717.307
62 3849.619 1697.053
63 3315.839 1708.413
64 3423.282 2104.829
65 3750.536 2822.277
66 3554.167 2610.241
67 3826.747 3645.146
68 3892.643 2795.429
69 3832.114 2572.367
70 3497.325 3586.324
71 3348.139 3108.224
72 3317.933 2944.826
73 3605.83 2890.459
74 3539.072 3132.536
75 3121.903 3343.355
76 2942.032 3478.153
77 3445.076 3762.927
78 3100.771 3377.621
79 3189.105 3326.58
80 3281.825 3443.852
81 2678.243 3830.363
82 2955.651 2863.628
83 2696.034 3640.54
84 3370.494 3203.94
85 3300.628 3755.641
86 3488.021 3931.192
87 3330.963 2780.609
88 3154.885 2986.501
89 3375.716 3359.562
90 2841.549 3077.406
91 3404.81 3385.657
92 3757.787 3352.594
93 3717.258 3264.236
94 3353.01 3659.337
95 3190.808 3732.121
96 3165.985 3380.969
97 3797.661 3264.325
98 3347.68 3711.328
99 3604.306 3454.656
100 3615.091 3547.976
101 3291.287 3115.255

View File

@@ -1,101 +1,101 @@
No filters,test data No filters,test data
4244.309,3423.133 4244.309,3423.133
4172.153,3839.874 4172.153,3839.874
4318.167,3651.161 4318.167,3651.161
4141.307,3886.542 4141.307,3886.542
4153.546,3293.166 4153.546,3293.166
4313.574,3639.47 4313.574,3639.47
4212.2,3422.614 4212.2,3422.614
3944.194,3928.898 3944.194,3928.898
3470.867,3395.562 3470.867,3395.562
3680.557,4233.545 3680.557,4233.545
3639.904,3739.869 3639.904,3739.869
3601.206,4331.278 3601.206,4331.278
3602.268,3561.573 3602.268,3561.573
4041.709,3360.442 4041.709,3360.442
3326.243,3898.576 3326.243,3898.576
3519.295,3710.73 3519.295,3710.73
3421.704,3785.601 3421.704,3785.601
3761.544,3720.579 3761.544,3720.579
3849.834,3419.051 3849.834,3419.051
3771.48,3525.297 3771.48,3525.297
3477.096,3709.462 3477.096,3709.462
3752.154,3410.653 3752.154,3410.653
3828.539,3784.068 3828.539,3784.068
3601.283,4371.022 3601.283,4371.022
3550.535,3353.485 3550.535,3353.485
3573.931,4326.953 3573.931,4326.953
3989.022,3630.239 3989.022,3630.239
3758.771,3187.932 3758.771,3187.932
3764.081,3348.153 3764.081,3348.153
3552.11,3210.788 3552.11,3210.788
3624.703,3580.683 3624.703,3580.683
3495.138,3702.232 3495.138,3702.232
3679.786,3211.763 3679.786,3211.763
3965.941,4386.728 3965.941,4386.728
3481.692,4312.93 3481.692,4312.93
3472.266,3638.52 3472.266,3638.52
3902.087,4356.89 3902.087,4356.89
4162.868,3770.82 4162.868,3770.82
3556.674,3899.06 3556.674,3899.06
3568.287,3768.694 3568.287,3768.694
3813.52,3794.494 3813.52,3794.494
3538.6,4233.813 3538.6,4233.813
3583.165,3598.301 3583.165,3598.301
3545.668,3574.602 3545.668,3574.602
3498.538,3731.551 3498.538,3731.551
4069.232,3732.176 4069.232,3732.176
3488.875,4390.112 3488.875,4390.112
3471.224,4308.19 3471.224,4308.19
3487.893,3713.36 3487.893,3713.36
3556.706,3783.748 3556.706,3783.748
4134.049,4075.267 4134.049,4075.267
3619.571,3616.779 3619.571,3616.779
3880.411,4017.523 3880.411,4017.523
3437.287,4024.127 3437.287,4024.127
3571.923,4136.496 3571.923,4136.496
3355.569,4297.359 3355.569,4297.359
3621.019,3428.405 3621.019,3428.405
3432.623,3962.733 3432.623,3962.733
3541.66,3558.748 3541.66,3558.748
3506.787,3874.117 3506.787,3874.117
4124.636,3616.127 4124.636,3616.127
3585.123,3360.593 3585.123,3360.593
3572.09,3416.381 3572.09,3416.381
3344.338,3861.743 3344.338,3861.743
3540.41,3412.915 3540.41,3412.915
3768.322,3490.888 3768.322,3490.888
3865.742,3149.312 3865.742,3149.312
3543.772,3438.211 3543.772,3438.211
3649.759,3538.124 3649.759,3538.124
3714.508,3298.845 3714.508,3298.845
3989.119,3652.572 3989.119,3652.572
4004.341,3688.486 4004.341,3688.486
3942.733,3533.375 3942.733,3533.375
3767.707,3692.636 3767.707,3692.636
3854.87,3567.363 3854.87,3567.363
3818.102,4325.471 3818.102,4325.471
4326.545,3464.113 4326.545,3464.113
3331.279,3346.4 3331.279,3346.4
3782.928,3599.129 3782.928,3599.129
3441.486,3571.214 3441.486,3571.214
3688.115,3778.354 3688.115,3778.354
3523.493,4268.157 3523.493,4268.157
3350.288,3241.872 3350.288,3241.872
3337.668,3405.69 3337.668,3405.69
3467.795,3655.209 3467.795,3655.209
3695.322,3161.427 3695.322,3161.427
4111.114,3289.313 4111.114,3289.313
3499.726,3157.723 3499.726,3157.723
3731.525,3334.048 3731.525,3334.048
4226.314,3315.567 4226.314,3315.567
3430.903,3176.271 3430.903,3176.271
3480.629,3296.73 3480.629,3296.73
3930.84,3302.929 3930.84,3302.929
3702.883,3251.164 3702.883,3251.164
3839.087,3180.461 3839.087,3180.461
3831.296,3215.8 3831.296,3215.8
3615.657,3262.533 3615.657,3262.533
3766.269,3446.736 3766.269,3446.736
3556.331,4274.897 3556.331,4274.897
3843.934,3370.384 3843.934,3370.384
1 No filters test data
2 4244.309 3423.133
3 4172.153 3839.874
4 4318.167 3651.161
5 4141.307 3886.542
6 4153.546 3293.166
7 4313.574 3639.47
8 4212.2 3422.614
9 3944.194 3928.898
10 3470.867 3395.562
11 3680.557 4233.545
12 3639.904 3739.869
13 3601.206 4331.278
14 3602.268 3561.573
15 4041.709 3360.442
16 3326.243 3898.576
17 3519.295 3710.73
18 3421.704 3785.601
19 3761.544 3720.579
20 3849.834 3419.051
21 3771.48 3525.297
22 3477.096 3709.462
23 3752.154 3410.653
24 3828.539 3784.068
25 3601.283 4371.022
26 3550.535 3353.485
27 3573.931 4326.953
28 3989.022 3630.239
29 3758.771 3187.932
30 3764.081 3348.153
31 3552.11 3210.788
32 3624.703 3580.683
33 3495.138 3702.232
34 3679.786 3211.763
35 3965.941 4386.728
36 3481.692 4312.93
37 3472.266 3638.52
38 3902.087 4356.89
39 4162.868 3770.82
40 3556.674 3899.06
41 3568.287 3768.694
42 3813.52 3794.494
43 3538.6 4233.813
44 3583.165 3598.301
45 3545.668 3574.602
46 3498.538 3731.551
47 4069.232 3732.176
48 3488.875 4390.112
49 3471.224 4308.19
50 3487.893 3713.36
51 3556.706 3783.748
52 4134.049 4075.267
53 3619.571 3616.779
54 3880.411 4017.523
55 3437.287 4024.127
56 3571.923 4136.496
57 3355.569 4297.359
58 3621.019 3428.405
59 3432.623 3962.733
60 3541.66 3558.748
61 3506.787 3874.117
62 4124.636 3616.127
63 3585.123 3360.593
64 3572.09 3416.381
65 3344.338 3861.743
66 3540.41 3412.915
67 3768.322 3490.888
68 3865.742 3149.312
69 3543.772 3438.211
70 3649.759 3538.124
71 3714.508 3298.845
72 3989.119 3652.572
73 4004.341 3688.486
74 3942.733 3533.375
75 3767.707 3692.636
76 3854.87 3567.363
77 3818.102 4325.471
78 4326.545 3464.113
79 3331.279 3346.4
80 3782.928 3599.129
81 3441.486 3571.214
82 3688.115 3778.354
83 3523.493 4268.157
84 3350.288 3241.872
85 3337.668 3405.69
86 3467.795 3655.209
87 3695.322 3161.427
88 4111.114 3289.313
89 3499.726 3157.723
90 3731.525 3334.048
91 4226.314 3315.567
92 3430.903 3176.271
93 3480.629 3296.73
94 3930.84 3302.929
95 3702.883 3251.164
96 3839.087 3180.461
97 3831.296 3215.8
98 3615.657 3262.533
99 3766.269 3446.736
100 3556.331 4274.897
101 3843.934 3370.384