Compare commits
6 Commits
16f96aa6f6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c387d9b40b | ||
|
|
d8061985d6 | ||
|
|
c237112077 | ||
|
|
811773e009 | ||
|
|
f1ebada95d | ||
|
|
9af3023a37 |
420
.github/workflows/docker-image.yml
vendored
420
.github/workflows/docker-image.yml
vendored
@@ -1,210 +1,210 @@
|
||||
name: Create and publish Docker images
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
docker_build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
run_tests: true
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
run_tests: true
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and run firegex
|
||||
if: matrix.run_tests
|
||||
run: python3 run.py start -P testpassword
|
||||
|
||||
- name: Run tests
|
||||
if: matrix.run_tests
|
||||
run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update version in setup.py
|
||||
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" fgex-lib/setup.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }}
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
docker_manifest:
|
||||
needs: docker_build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and push multi-platform manifest
|
||||
run: |
|
||||
# Create manifest list for specific tag
|
||||
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 }}-arm64
|
||||
|
||||
# Annotate the manifest with architecture info
|
||||
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 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
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 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}
|
||||
|
||||
# Create manifest list for latest tag
|
||||
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-arm64
|
||||
|
||||
# Annotate the latest manifest with architecture info
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest
|
||||
|
||||
create-rootfs-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [docker_manifest]
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get latest release tag
|
||||
id: get_tag
|
||||
run: |
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')
|
||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Latest release tag: $LATEST_TAG"
|
||||
|
||||
- name: Export rootfs for amd64
|
||||
run: |
|
||||
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 }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing amd64 rootfs..."
|
||||
gzip firegex-rootfs-amd64.tar
|
||||
ls -lh firegex-rootfs-amd64.tar.gz
|
||||
|
||||
- name: Export rootfs for arm64
|
||||
run: |
|
||||
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 }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing arm64 rootfs..."
|
||||
gzip firegex-rootfs-arm64.tar
|
||||
ls -lh firegex-rootfs-arm64.tar.gz
|
||||
|
||||
- name: Upload rootfs assets to release
|
||||
run: |
|
||||
echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..."
|
||||
gh release upload ${{ steps.get_tag.outputs.tag }} \
|
||||
firegex-rootfs-amd64.tar.gz \
|
||||
firegex-rootfs-arm64.tar.gz \
|
||||
--clobber
|
||||
echo "Assets uploaded successfully!"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: Create and publish Docker images
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
docker_build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
run_tests: true
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
run_tests: true
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and run firegex
|
||||
if: matrix.run_tests
|
||||
run: python3 run.py start -P testpassword
|
||||
|
||||
- name: Run tests
|
||||
if: matrix.run_tests
|
||||
run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update version in setup.py
|
||||
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" fgex-lib/setup.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }}
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
docker_manifest:
|
||||
needs: docker_build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and push multi-platform manifest
|
||||
run: |
|
||||
# Create manifest list for specific tag
|
||||
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 }}-arm64
|
||||
|
||||
# Annotate the manifest with architecture info
|
||||
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 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
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 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}
|
||||
|
||||
# Create manifest list for latest tag
|
||||
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-arm64
|
||||
|
||||
# Annotate the latest manifest with architecture info
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest
|
||||
|
||||
create-rootfs-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [docker_manifest]
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get latest release tag
|
||||
id: get_tag
|
||||
run: |
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')
|
||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Latest release tag: $LATEST_TAG"
|
||||
|
||||
- name: Export rootfs for amd64
|
||||
run: |
|
||||
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 }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing amd64 rootfs..."
|
||||
gzip firegex-rootfs-amd64.tar
|
||||
ls -lh firegex-rootfs-amd64.tar.gz
|
||||
|
||||
- name: Export rootfs for arm64
|
||||
run: |
|
||||
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 }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing arm64 rootfs..."
|
||||
gzip firegex-rootfs-arm64.tar
|
||||
ls -lh firegex-rootfs-arm64.tar.gz
|
||||
|
||||
- name: Upload rootfs assets to release
|
||||
run: |
|
||||
echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..."
|
||||
gh release upload ${{ steps.get_tag.outputs.tag }} \
|
||||
firegex-rootfs-amd64.tar.gz \
|
||||
firegex-rootfs-arm64.tar.gz \
|
||||
--clobber
|
||||
echo "Assets uploaded successfully!"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
92
.github/workflows/pypi-publish-fgex.yml
vendored
92
.github/workflows/pypi-publish-fgex.yml
vendored
@@ -1,46 +1,46 @@
|
||||
# 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
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Upload Python Package (fgex alias)
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
- name: Update version in setup.py
|
||||
run: >-
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
||||
- name: Build package
|
||||
run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN_FGEX }}
|
||||
# # 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
|
||||
|
||||
# # This workflow uses actions that are not certified by GitHub.
|
||||
# # They are provided by a third-party and are governed by
|
||||
# # separate terms of service, privacy policy, and support
|
||||
# # documentation.
|
||||
|
||||
# name: Upload Python Package (fgex alias)
|
||||
|
||||
# on:
|
||||
# release:
|
||||
# types:
|
||||
# - published
|
||||
|
||||
# permissions:
|
||||
# contents: read
|
||||
|
||||
# jobs:
|
||||
# deploy:
|
||||
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: '3.x'
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip
|
||||
# pip install build
|
||||
# - name: Extract tag name
|
||||
# id: tag
|
||||
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
# - name: Update version in setup.py
|
||||
# run: >-
|
||||
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
||||
# - name: Build package
|
||||
# run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
||||
# - name: Publish package
|
||||
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
# with:
|
||||
# user: __token__
|
||||
# password: ${{ secrets.PYPI_API_TOKEN_FGEX }}
|
||||
|
||||
94
.github/workflows/pypi-publish.yml
vendored
94
.github/workflows/pypi-publish.yml
vendored
@@ -1,47 +1,47 @@
|
||||
# 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
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
- name: Update version in setup.py
|
||||
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/firegex/__init__.py;
|
||||
- name: Build package
|
||||
run: cd fgex-lib && python -m build && mv ./dist ../
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
# # 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
|
||||
|
||||
# # This workflow uses actions that are not certified by GitHub.
|
||||
# # They are provided by a third-party and are governed by
|
||||
# # separate terms of service, privacy policy, and support
|
||||
# # documentation.
|
||||
|
||||
# name: Upload Python Package
|
||||
|
||||
# on:
|
||||
# release:
|
||||
# types:
|
||||
# - published
|
||||
|
||||
# permissions:
|
||||
# contents: read
|
||||
|
||||
# jobs:
|
||||
# deploy:
|
||||
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: '3.x'
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip
|
||||
# pip install build
|
||||
# - name: Extract tag name
|
||||
# id: tag
|
||||
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
# - name: Update version in setup.py
|
||||
# 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/firegex/__init__.py;
|
||||
# - name: Build package
|
||||
# run: cd fgex-lib && python -m build && mv ./dist ../
|
||||
# - name: Publish package
|
||||
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
# with:
|
||||
# user: __token__
|
||||
# password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
|
||||
112
Dockerfile
112
Dockerfile
@@ -1,49 +1,63 @@
|
||||
|
||||
# Firegex Dockerfile UUID signature
|
||||
# cf1795af-3284-4183-a888-81ad3590ad84
|
||||
# Needed for run.py to detect the Dockerfile
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM oven/bun AS frontend
|
||||
WORKDIR /app
|
||||
ADD ./frontend/package.json .
|
||||
ADD ./frontend/bun.lock .
|
||||
RUN bun i
|
||||
COPY ./frontend/ .
|
||||
RUN bun run build
|
||||
|
||||
# Base fedora container
|
||||
FROM --platform=$TARGETARCH quay.io/fedora/fedora:43 AS base
|
||||
RUN dnf -y update && dnf install -y python3.14 libnetfilter_queue \
|
||||
libnfnetlink libmnl libcap-ng-utils nftables \
|
||||
vectorscan libtins python3-nftables libpcap && dnf clean all
|
||||
|
||||
RUN mkdir -p /execute/modules
|
||||
WORKDIR /execute
|
||||
|
||||
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 \
|
||||
vectorscan-devel libtins-devel libpcap-devel boost-devel
|
||||
|
||||
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)
|
||||
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.14 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
|
||||
|
||||
#Building main conteiner
|
||||
FROM --platform=$TARGETARCH base AS final
|
||||
|
||||
COPY ./backend/requirements.txt /execute/requirements.txt
|
||||
COPY ./fgex-lib /execute/fgex-lib
|
||||
|
||||
RUN dnf -y update && dnf install -y gcc-c++ python3.14-devel uv git &&\
|
||||
uv pip install --no-cache --system ./fgex-lib &&\
|
||||
uv pip install --no-cache --system -r /execute/requirements.txt &&\
|
||||
uv cache clean && dnf remove -y gcc-c++ python3.14-devel uv git && dnf clean all
|
||||
|
||||
COPY ./backend/ /execute/
|
||||
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
|
||||
COPY --from=frontend /app/dist/ ./frontend/
|
||||
|
||||
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"]
|
||||
|
||||
# Firegex Dockerfile UUID signature
|
||||
# cf1795af-3284-4183-a888-81ad3590ad84
|
||||
# Needed for run.py to detect the Dockerfile
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM oven/bun AS frontend
|
||||
WORKDIR /app
|
||||
ADD ./frontend/package.json .
|
||||
ADD ./frontend/bun.lock .
|
||||
RUN bun i
|
||||
COPY ./frontend/ .
|
||||
RUN bun run build
|
||||
|
||||
# Base Ubuntu container
|
||||
FROM --platform=$TARGETARCH ubuntu:24.04 AS base
|
||||
RUN apt-get update && apt-get install -y python3 libnetfilter-queue1 \
|
||||
libnfnetlink0 libmnl0 libcap-ng-utils libcap2-bin nftables \
|
||||
libhyperscan5 python3-nftables libpcap0.8 && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /execute/modules
|
||||
WORKDIR /execute
|
||||
|
||||
FROM --platform=$TARGETARCH base AS compiler
|
||||
|
||||
RUN apt-get update && apt-get install -y python3-dev build-essential g++ \
|
||||
libnetfilter-queue-dev libnfnetlink-dev libmnl-dev \
|
||||
libhyperscan-dev libpcap-dev libboost-dev pkg-config wget cmake && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Build libtins from source as it's not available in Ubuntu 24.04
|
||||
RUN wget https://github.com/mfontanini/libtins/archive/v4.5.tar.gz && \
|
||||
tar -xzf v4.5.tar.gz && cd libtins-4.5 && \
|
||||
mkdir build && cd build && \
|
||||
cmake ../ -DLIBTINS_ENABLE_CXX11=1 && \
|
||||
make && make install && ldconfig && \
|
||||
cd ../.. && rm -rf libtins-4.5 v4.5.tar.gz
|
||||
|
||||
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)
|
||||
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.12 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
|
||||
|
||||
#Building main conteiner
|
||||
FROM --platform=$TARGETARCH base AS final
|
||||
|
||||
COPY ./backend/requirements.txt /execute/requirements.txt
|
||||
COPY ./fgex-lib /execute/fgex-lib
|
||||
|
||||
RUN apt-get update && apt-get install -y g++ python3-dev python3-pip git && \
|
||||
pip3 install --no-cache-dir --break-system-packages ./fgex-lib && \
|
||||
pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt && \
|
||||
apt-get remove -y g++ python3-dev git && \
|
||||
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY ./backend/ /execute/
|
||||
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
|
||||
COPY --from=compiler /usr/local/lib/libtins* /usr/local/lib/
|
||||
COPY --from=frontend /app/dist/ ./frontend/
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"]
|
||||
|
||||
31
README.md
31
README.md
@@ -68,6 +68,37 @@ All the configuration at the startup is customizable in [firegex.py](./run.py) o
|
||||
- Create basic firewall rules to allow and deny specific traffic, like ufw or iptables but using firegex graphic interface (by using [nftable](https://netfilter.org/projects/nftables/))
|
||||
- Port Hijacking allows you to redirect the traffic on a specific port to another port. Thanks to this you can start your own proxy, connecting to the real service using the loopback interface. Firegex will be resposable about the routing of the packets using internally [nftables](https://netfilter.org/projects/nftables/)
|
||||
- EXPERIMENTAL: Netfilter Proxy uses [nfqueue](https://netfilter.org/projects/libnetfilter_queue/) to simulate a python proxy, you can write your own filter in python and use it to filter the traffic. There are built-in some data handler to parse protocols like HTTP, and before apply the filter you can test it with fgex command (you need to install firegex lib from pypi).
|
||||
- Traffic Viewer allows you to monitor live network traffic for all services in real-time
|
||||
- Setup Import/Export allows you to backup and restore your entire Firegex configuration as a JSON file, making it easy to deploy identical configurations across multiple servers
|
||||
|
||||
## Configuration Management
|
||||
|
||||
Firegex supports importing and exporting configurations via JSON files. This is useful for:
|
||||
- Backing up your configuration
|
||||
- Deploying the same setup across multiple servers
|
||||
- Version controlling your firewall rules
|
||||
- Quick disaster recovery
|
||||
|
||||
### Using the Web Interface
|
||||
Navigate to "Setup Import/Export" in the sidebar to:
|
||||
- **Export**: Download your current configuration as JSON
|
||||
- **Import from File**: Upload a setup.json file
|
||||
- **Import from JSON**: Paste JSON directly into the interface
|
||||
|
||||
### Using the API
|
||||
```bash
|
||||
# Export configuration
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
http://localhost:4444/api/setup/export > setup.json
|
||||
|
||||
# Import configuration
|
||||
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @setup.json \
|
||||
http://localhost:4444/api/setup/import
|
||||
```
|
||||
|
||||
See [setup.example.json](setup.example.json) for the configuration file format.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import asyncio
|
||||
import traceback
|
||||
from fastapi import HTTPException
|
||||
import time
|
||||
import json
|
||||
from utils import run_func
|
||||
from utils import DEBUG
|
||||
from utils import nicenessify
|
||||
@@ -35,11 +36,12 @@ class FiregexInterceptor:
|
||||
self.last_time_exception = 0
|
||||
self.outstrem_function = None
|
||||
self.expection_function = None
|
||||
self.traffic_function = None
|
||||
self.outstrem_task: asyncio.Task
|
||||
self.outstrem_buffer = ""
|
||||
|
||||
@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.srv = srv
|
||||
self.filter_map_lock = asyncio.Lock()
|
||||
@@ -47,6 +49,7 @@ class FiregexInterceptor:
|
||||
self.sock_conn_lock = asyncio.Lock()
|
||||
self.outstrem_function = outstream_func
|
||||
self.expection_function = exception_func
|
||||
self.traffic_function = traffic_func
|
||||
if not self.sock_conn_lock.locked():
|
||||
await self.sock_conn_lock.acquire()
|
||||
self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock"
|
||||
@@ -83,9 +86,21 @@ class FiregexInterceptor:
|
||||
self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n"
|
||||
if self.outstrem_function:
|
||||
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):
|
||||
proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))
|
||||
# Determine match mode based on protocol
|
||||
match_mode = "stream" if self.srv.proto in ["tcp", "http"] else "block"
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
@@ -93,7 +108,9 @@ class FiregexInterceptor:
|
||||
env={
|
||||
"NTHREADS": os.getenv("NTHREADS","1"),
|
||||
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
||||
"FIREGEX_NFPROXY_SOCK": self.sock_path
|
||||
"FIREGEX_NFPROXY_SOCK": self.sock_path,
|
||||
"MATCH_MODE": match_mode,
|
||||
"PROTOCOL": self.srv.proto
|
||||
},
|
||||
)
|
||||
nicenessify(-10, self.process.pid)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from modules.nfproxy.firegex import FiregexInterceptor
|
||||
from modules.nfproxy.nftables import FiregexTables, FiregexFilter
|
||||
from modules.nfproxy.models import Service, PyFilter
|
||||
@@ -12,7 +13,7 @@ class STATUS:
|
||||
nft = FiregexTables()
|
||||
|
||||
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.db = db
|
||||
self.status = STATUS.STOP
|
||||
@@ -21,11 +22,17 @@ class ServiceManager:
|
||||
self.interceptor = None
|
||||
self.outstream_function = outstream_func
|
||||
self.last_exception_time = 0
|
||||
self.traffic_events = deque(maxlen=500) # Ring buffer for traffic viewer
|
||||
async def excep_internal_handler(srv, exc_time):
|
||||
self.last_exception_time = exc_time
|
||||
if exception_func:
|
||||
await run_func(exception_func, srv, exc_time)
|
||||
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):
|
||||
pyfilters = [
|
||||
@@ -69,7 +76,7 @@ class ServiceManager:
|
||||
async def start(self):
|
||||
if not self.interceptor:
|
||||
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()
|
||||
self._set_status(STATUS.ACTIVE)
|
||||
|
||||
@@ -87,14 +94,24 @@ class ServiceManager:
|
||||
async def update_filters(self):
|
||||
async with self.lock:
|
||||
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:
|
||||
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.service_table: dict[str, ServiceManager] = {}
|
||||
self.lock = asyncio.Lock()
|
||||
self.outstream_function = outstream_func
|
||||
self.exception_function = exception_func
|
||||
self.traffic_function = traffic_func
|
||||
|
||||
async def close(self):
|
||||
for key in list(self.service_table.keys()):
|
||||
@@ -116,7 +133,7 @@ class FirewallManager:
|
||||
srv = Service.from_dict(srv)
|
||||
if srv.id in self.service_table:
|
||||
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)
|
||||
|
||||
def get(self,srv_id) -> ServiceManager:
|
||||
|
||||
@@ -6,6 +6,8 @@ def convert_protocol_to_l4(proto:str):
|
||||
return "tcp"
|
||||
elif proto == "http":
|
||||
return "tcp"
|
||||
elif proto == "udp":
|
||||
return "udp"
|
||||
else:
|
||||
raise Exception("Invalid protocol")
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
fastapi[all]
|
||||
httpx
|
||||
uvicorn[standard]
|
||||
psutil
|
||||
python-jose[cryptography]
|
||||
python-socketio
|
||||
brotli
|
||||
#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py
|
||||
fastapi[all]
|
||||
httpx
|
||||
uvicorn[standard]
|
||||
psutil
|
||||
python-jose[cryptography]
|
||||
python-socketio
|
||||
brotli
|
||||
zstandard
|
||||
#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py
|
||||
|
||||
@@ -65,7 +65,7 @@ db = SQLite('db/nft-pyfilters.db', {
|
||||
'status': 'VARCHAR(100) NOT NULL',
|
||||
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
|
||||
'name': 'VARCHAR(100) NOT NULL UNIQUE',
|
||||
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "http"))',
|
||||
'proto': 'VARCHAR(4) NOT NULL CHECK (proto IN ("tcp", "http", "udp"))',
|
||||
'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))',
|
||||
'ip_int': 'VARCHAR(100) NOT NULL',
|
||||
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
|
||||
@@ -113,6 +113,8 @@ async def startup():
|
||||
utils.socketio.on("nfproxy-outstream-leave", leave_outstream)
|
||||
utils.socketio.on("nfproxy-exception-join", join_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():
|
||||
db.backup()
|
||||
@@ -133,7 +135,10 @@ async def outstream_func(service_id, data):
|
||||
async def exception_func(service_id, timestamp):
|
||||
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])
|
||||
async def get_service_list():
|
||||
@@ -300,7 +305,7 @@ async def add_new_service(form: ServiceAddForm):
|
||||
form.ip_int = ip_parse(form.ip_int)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid address")
|
||||
if form.proto not in ["tcp", "http"]:
|
||||
if form.proto not in ["tcp", "http", "udp"]:
|
||||
raise HTTPException(status_code=400, detail="Invalid protocol")
|
||||
srv_id = None
|
||||
try:
|
||||
@@ -368,6 +373,28 @@ async def get_pyfilters_code(service_id: str):
|
||||
except FileNotFoundError:
|
||||
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
|
||||
async def join_outstream(sid, data):
|
||||
"""Client joins a room."""
|
||||
@@ -397,3 +424,20 @@ async def leave_exception(sid, data):
|
||||
if 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}")
|
||||
|
||||
223
backend/routers/setup.py
Normal file
223
backend/routers/setup.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
from typing import List, Optional
|
||||
from utils.models import StatusMessageModel
|
||||
from routers import nfproxy, nfregex, porthijack, firewall
|
||||
|
||||
class ServiceConfig(BaseModel):
|
||||
name: str
|
||||
port: int
|
||||
proto: str
|
||||
ip_int: str
|
||||
fail_open: bool = True
|
||||
|
||||
class PortHijackServiceConfig(BaseModel):
|
||||
name: str
|
||||
public_port: int
|
||||
proxy_port: int
|
||||
proto: str
|
||||
ip_src: str
|
||||
ip_dst: str
|
||||
|
||||
class FirewallRuleConfig(BaseModel):
|
||||
mode: str
|
||||
src: str
|
||||
dst: str
|
||||
in_int: str
|
||||
out_int: str
|
||||
proto: str
|
||||
sport: str
|
||||
dport: str
|
||||
|
||||
class SetupConfig(BaseModel):
|
||||
services: Optional[List[ServiceConfig]] = []
|
||||
porthijack: Optional[List[PortHijackServiceConfig]] = []
|
||||
firewall: Optional[List[FirewallRuleConfig]] = []
|
||||
|
||||
class SetupResponse(BaseModel):
|
||||
status: str
|
||||
services_created: int = 0
|
||||
porthijack_created: int = 0
|
||||
firewall_created: int = 0
|
||||
errors: List[str] = []
|
||||
|
||||
app = APIRouter()
|
||||
|
||||
@app.post("/import", response_model=SetupResponse)
|
||||
async def import_setup(config: SetupConfig):
|
||||
"""
|
||||
Import services and rules from a setup configuration.
|
||||
Creates basic services without filters or regex rules.
|
||||
"""
|
||||
errors = []
|
||||
services_count = 0
|
||||
porthijack_count = 0
|
||||
firewall_count = 0
|
||||
|
||||
# Import Services
|
||||
if config.services:
|
||||
for service_config in config.services:
|
||||
try:
|
||||
# Determine which module to use based on protocol
|
||||
# HTTP -> NFProxy, TCP/UDP -> can use either (prefer NFProxy)
|
||||
if service_config.proto in ["tcp", "http", "udp"]:
|
||||
# Create NFProxy service
|
||||
try:
|
||||
add_form = nfproxy.ServiceAddForm(
|
||||
name=service_config.name,
|
||||
port=service_config.port,
|
||||
proto=service_config.proto,
|
||||
ip_int=service_config.ip_int,
|
||||
fail_open=service_config.fail_open
|
||||
)
|
||||
result = await nfproxy.add_service(add_form)
|
||||
|
||||
if result.status == "ok":
|
||||
services_count += 1
|
||||
else:
|
||||
errors.append(f"Service '{service_config.name}': Failed to create")
|
||||
except Exception as e:
|
||||
errors.append(f"Service '{service_config.name}': {str(e)}")
|
||||
else:
|
||||
errors.append(f"Service '{service_config.name}': Unsupported protocol '{service_config.proto}'")
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Service '{service_config.name}': {str(e)}")
|
||||
|
||||
# Import PortHijack services
|
||||
if config.porthijack:
|
||||
for service_config in config.porthijack:
|
||||
try:
|
||||
add_form = porthijack.ServiceAddForm(
|
||||
name=service_config.name,
|
||||
public_port=service_config.public_port,
|
||||
proxy_port=service_config.proxy_port,
|
||||
proto=service_config.proto,
|
||||
ip_src=service_config.ip_src,
|
||||
ip_dst=service_config.ip_dst
|
||||
)
|
||||
result = await porthijack.add_service(add_form)
|
||||
|
||||
if result.status == "ok":
|
||||
porthijack_count += 1
|
||||
else:
|
||||
errors.append(f"PortHijack service '{service_config.name}': Failed to create")
|
||||
except Exception as e:
|
||||
errors.append(f"PortHijack service '{service_config.name}': {str(e)}")
|
||||
|
||||
# Import Firewall rules
|
||||
if config.firewall:
|
||||
for rule_config in config.firewall:
|
||||
try:
|
||||
rule_form = firewall.RuleFormAdd(
|
||||
mode=rule_config.mode,
|
||||
src=rule_config.src,
|
||||
dst=rule_config.dst,
|
||||
in_int=rule_config.in_int,
|
||||
out_int=rule_config.out_int,
|
||||
proto=rule_config.proto,
|
||||
sport=rule_config.sport,
|
||||
dport=rule_config.dport
|
||||
)
|
||||
await firewall.add_rule(rule_form)
|
||||
firewall_count += 1
|
||||
except Exception as e:
|
||||
errors.append(f"Firewall rule: {str(e)}")
|
||||
|
||||
return SetupResponse(
|
||||
status="ok" if len(errors) == 0 else "partial",
|
||||
services_created=services_count,
|
||||
porthijack_created=porthijack_count,
|
||||
firewall_created=firewall_count,
|
||||
errors=errors
|
||||
)
|
||||
|
||||
@app.post("/import/file")
|
||||
async def import_setup_file(file: UploadFile = File(...)):
|
||||
"""
|
||||
Import services from an uploaded JSON file.
|
||||
"""
|
||||
try:
|
||||
content = await file.read()
|
||||
config_dict = json.loads(content.decode('utf-8'))
|
||||
config = SetupConfig(**config_dict)
|
||||
return await import_setup(config)
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid JSON: {str(e)}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Error processing file: {str(e)}")
|
||||
|
||||
@app.get("/export")
|
||||
async def export_setup():
|
||||
"""
|
||||
Export all current services and rules as a JSON configuration.
|
||||
Exports only service definitions without filters or regexes.
|
||||
"""
|
||||
config = {
|
||||
"services": [],
|
||||
"porthijack": [],
|
||||
"firewall": []
|
||||
}
|
||||
|
||||
# Export NFProxy services
|
||||
try:
|
||||
nfproxy_services = await nfproxy.get_services()
|
||||
for service in nfproxy_services:
|
||||
config["services"].append({
|
||||
"name": service.name,
|
||||
"port": service.port,
|
||||
"proto": service.proto,
|
||||
"ip_int": service.ip_int,
|
||||
"fail_open": service.fail_open
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
# Export NFRegex services
|
||||
try:
|
||||
nfregex_services = await nfregex.get_services()
|
||||
for service in nfregex_services:
|
||||
config["services"].append({
|
||||
"name": service.name,
|
||||
"port": service.port,
|
||||
"proto": service.proto,
|
||||
"ip_int": service.ip_int,
|
||||
"fail_open": service.fail_open
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
# Export PortHijack services
|
||||
try:
|
||||
porthijack_services = await porthijack.get_services()
|
||||
for service in porthijack_services:
|
||||
config["porthijack"].append({
|
||||
"name": service.name,
|
||||
"public_port": service.public_port,
|
||||
"proxy_port": service.proxy_port,
|
||||
"proto": service.proto,
|
||||
"ip_src": service.ip_src,
|
||||
"ip_dst": service.ip_dst
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
# Export Firewall rules
|
||||
try:
|
||||
fw_rules = await firewall.get_rules()
|
||||
for rule in fw_rules:
|
||||
config["firewall"].append({
|
||||
"mode": rule.mode,
|
||||
"src": rule.src,
|
||||
"dst": rule.dst,
|
||||
"in_int": rule.in_int,
|
||||
"out_int": rule.out_int,
|
||||
"proto": rule.proto,
|
||||
"sport": rule.sport,
|
||||
"dport": rule.dport
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
return config
|
||||
@@ -1,221 +1,221 @@
|
||||
import asyncio
|
||||
from ipaddress import ip_address, ip_interface
|
||||
import os
|
||||
import socket
|
||||
import psutil
|
||||
import sys
|
||||
import nftables
|
||||
from socketio import AsyncServer
|
||||
from fastapi import Path
|
||||
from typing import Annotated
|
||||
from functools import wraps
|
||||
from pydantic import BaseModel, ValidationError
|
||||
import traceback
|
||||
from utils.models import StatusMessageModel
|
||||
from typing import List
|
||||
|
||||
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
|
||||
|
||||
socketio:AsyncServer = None
|
||||
sid_list:set = set()
|
||||
|
||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
|
||||
ON_DOCKER = "DOCKER" in sys.argv
|
||||
DEBUG = "DEBUG" in sys.argv
|
||||
NORELOAD = "NORELOAD" in sys.argv
|
||||
FIREGEX_PORT = int(os.getenv("PORT","4444"))
|
||||
FIREGEX_HOST = os.getenv("HOST","0.0.0.0")
|
||||
FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None)
|
||||
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
|
||||
|
||||
PortType = Annotated[int, Path(gt=0, lt=65536)]
|
||||
|
||||
async def run_func(func, *args, **kwargs):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
async def socketio_emit(elements:list[str]):
|
||||
await socketio.emit("update",elements)
|
||||
|
||||
def refactor_name(name:str):
|
||||
name = name.strip()
|
||||
while " " in name:
|
||||
name = name.replace(" "," ")
|
||||
return name
|
||||
|
||||
class SysctlManager:
|
||||
def __init__(self, ctl_table):
|
||||
self.old_table = {}
|
||||
self.new_table = {}
|
||||
if os.path.isdir("/sys_host/"):
|
||||
self.old_table = dict()
|
||||
self.new_table = dict(ctl_table)
|
||||
for name in ctl_table.keys():
|
||||
self.old_table[name] = read_sysctl(name)
|
||||
|
||||
def write_table(self, table) -> bool:
|
||||
for name, value in table.items():
|
||||
if read_sysctl(name) != value:
|
||||
write_sysctl(name, value)
|
||||
|
||||
def set(self):
|
||||
self.write_table(self.new_table)
|
||||
|
||||
def reset(self):
|
||||
self.write_table(self.old_table)
|
||||
|
||||
def read_sysctl(name:str):
|
||||
with open(f"/sys_host/{name}", "rt") as f:
|
||||
return "1" in f.read()
|
||||
|
||||
def write_sysctl(name:str, value:bool):
|
||||
with open(f"/sys_host/{name}", "wt") as f:
|
||||
f.write("1" if value else "0")
|
||||
|
||||
def list_files(mypath):
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
return [f for f in listdir(mypath) if isfile(join(mypath, f))]
|
||||
|
||||
def ip_parse(ip:str):
|
||||
return str(ip_interface(ip).network)
|
||||
|
||||
def is_ip_parse(ip:str):
|
||||
try:
|
||||
ip_parse(ip)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def addr_parse(ip:str):
|
||||
return str(ip_address(ip))
|
||||
|
||||
def ip_family(ip:str):
|
||||
return "ip6" if ip_interface(ip).version == 6 else "ip"
|
||||
|
||||
def get_interfaces():
|
||||
def _get_interfaces():
|
||||
for int_name, interfs in psutil.net_if_addrs().items():
|
||||
for interf in interfs:
|
||||
if interf.family in [socket.AF_INET, socket.AF_INET6]:
|
||||
yield {"name": int_name, "addr":interf.address}
|
||||
return list(_get_interfaces())
|
||||
|
||||
def nftables_int_to_json(ip_int):
|
||||
ip_int = ip_parse(ip_int)
|
||||
ip_addr = str(ip_int).split("/")[0]
|
||||
ip_addr_cidr = int(str(ip_int).split("/")[1])
|
||||
return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}
|
||||
|
||||
def nftables_json_to_int(ip_json_int):
|
||||
if isinstance(ip_json_int,str):
|
||||
return str(ip_parse(ip_json_int))
|
||||
else:
|
||||
return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}'
|
||||
|
||||
class Singleton(object):
|
||||
__instance = None
|
||||
def __new__(class_, *args, **kwargs):
|
||||
if not isinstance(class_.__instance, class_):
|
||||
class_.__instance = object.__new__(class_, *args, **kwargs)
|
||||
return class_.__instance
|
||||
|
||||
class NFTableManager(Singleton):
|
||||
|
||||
table_name = "firegex"
|
||||
|
||||
def __init__(self, init_cmd, reset_cmd):
|
||||
self.__init_cmds = init_cmd
|
||||
self.__reset_cmds = reset_cmd
|
||||
self.nft = nftables.Nftables()
|
||||
|
||||
def raw_cmd(self, *cmds):
|
||||
return self.nft.json_cmd({"nftables": list(cmds)})
|
||||
|
||||
def cmd(self, *cmds):
|
||||
code, out, err = self.raw_cmd(*cmds)
|
||||
if code == 0:
|
||||
return out
|
||||
else:
|
||||
raise Exception(err)
|
||||
|
||||
def init(self):
|
||||
self.reset()
|
||||
self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}})
|
||||
self.cmd(*self.__init_cmds)
|
||||
|
||||
def reset(self):
|
||||
self.raw_cmd(*self.__reset_cmds)
|
||||
|
||||
def list_rules(self, tables = None, chains = None):
|
||||
for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]:
|
||||
if tables and filter["table"] not in tables:
|
||||
continue
|
||||
if chains and filter["chain"] not in chains:
|
||||
continue
|
||||
yield filter
|
||||
|
||||
def raw_list(self):
|
||||
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"):
|
||||
res = obj.model_dump(mode=mode, exclude_unset=not unset)
|
||||
if convert_keys:
|
||||
for from_k, to_k in convert_keys.items():
|
||||
if from_k in res:
|
||||
res[to_k] = res.pop(from_k)
|
||||
if exclude:
|
||||
for ele in exclude:
|
||||
if ele in res:
|
||||
del res[ele]
|
||||
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:
|
||||
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(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 decorator(func):
|
||||
@sio_server.on(event_name) # Automatically registers the event
|
||||
@wraps(func)
|
||||
async def wrapper(sid, data):
|
||||
try:
|
||||
# Parse and validate incoming data
|
||||
parsed_data = model.model_validate(data)
|
||||
except ValidationError:
|
||||
return json_like(StatusMessageModel(status=f"Invalid {event_name} request"))
|
||||
|
||||
# Call the original function with the parsed data
|
||||
result = await func(sid, parsed_data)
|
||||
# If a response model is provided, validate the output
|
||||
if response_model:
|
||||
try:
|
||||
parsed_result = response_model.model_validate(result)
|
||||
except ValidationError:
|
||||
traceback.print_exc()
|
||||
return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response"))
|
||||
else:
|
||||
parsed_result = result
|
||||
# Emit the validated result
|
||||
if parsed_result:
|
||||
if isinstance(parsed_result, BaseModel):
|
||||
return json_like(parsed_result)
|
||||
return parsed_result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def nicenessify(priority:int, pid:int|None=None):
|
||||
try:
|
||||
pid = os.getpid() if pid is None else pid
|
||||
ps = psutil.Process(pid)
|
||||
if os.name == 'posix':
|
||||
ps.nice(priority)
|
||||
except Exception as e:
|
||||
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
||||
pass
|
||||
import asyncio
|
||||
from ipaddress import ip_address, ip_interface
|
||||
import os
|
||||
import socket
|
||||
import psutil
|
||||
import sys
|
||||
import nftables
|
||||
from socketio import AsyncServer
|
||||
from fastapi import Path
|
||||
from typing import Annotated
|
||||
from functools import wraps
|
||||
from pydantic import BaseModel, ValidationError
|
||||
import traceback
|
||||
from utils.models import StatusMessageModel
|
||||
from typing import List
|
||||
|
||||
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
|
||||
|
||||
socketio:AsyncServer = None
|
||||
sid_list:set = set()
|
||||
|
||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
|
||||
ON_DOCKER = "DOCKER" in sys.argv
|
||||
DEBUG = "DEBUG" in sys.argv
|
||||
NORELOAD = "NORELOAD" in sys.argv
|
||||
FIREGEX_PORT = int(os.getenv("PORT","4444"))
|
||||
FIREGEX_HOST = os.getenv("HOST","0.0.0.0")
|
||||
FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None)
|
||||
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
|
||||
|
||||
PortType = Annotated[int, Path(gt=0, lt=65536)]
|
||||
|
||||
async def run_func(func, *args, **kwargs):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
async def socketio_emit(elements:list[str]):
|
||||
await socketio.emit("update",elements)
|
||||
|
||||
def refactor_name(name:str):
|
||||
name = name.strip()
|
||||
while " " in name:
|
||||
name = name.replace(" "," ")
|
||||
return name
|
||||
|
||||
class SysctlManager:
|
||||
def __init__(self, ctl_table):
|
||||
self.old_table = {}
|
||||
self.new_table = {}
|
||||
if os.path.isdir("/sys_host/"):
|
||||
self.old_table = dict()
|
||||
self.new_table = dict(ctl_table)
|
||||
for name in ctl_table.keys():
|
||||
self.old_table[name] = read_sysctl(name)
|
||||
|
||||
def write_table(self, table) -> bool:
|
||||
for name, value in table.items():
|
||||
if read_sysctl(name) != value:
|
||||
write_sysctl(name, value)
|
||||
|
||||
def set(self):
|
||||
self.write_table(self.new_table)
|
||||
|
||||
def reset(self):
|
||||
self.write_table(self.old_table)
|
||||
|
||||
def read_sysctl(name:str):
|
||||
with open(f"/sys_host/{name}", "rt") as f:
|
||||
return "1" in f.read()
|
||||
|
||||
def write_sysctl(name:str, value:bool):
|
||||
with open(f"/sys_host/{name}", "wt") as f:
|
||||
f.write("1" if value else "0")
|
||||
|
||||
def list_files(mypath):
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
return [f for f in listdir(mypath) if isfile(join(mypath, f))]
|
||||
|
||||
def ip_parse(ip:str):
|
||||
return str(ip_interface(ip).network)
|
||||
|
||||
def is_ip_parse(ip:str):
|
||||
try:
|
||||
ip_parse(ip)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def addr_parse(ip:str):
|
||||
return str(ip_address(ip))
|
||||
|
||||
def ip_family(ip:str):
|
||||
return "ip6" if ip_interface(ip).version == 6 else "ip"
|
||||
|
||||
def get_interfaces():
|
||||
def _get_interfaces():
|
||||
for int_name, interfs in psutil.net_if_addrs().items():
|
||||
for interf in interfs:
|
||||
if interf.family in [socket.AF_INET, socket.AF_INET6]:
|
||||
yield {"name": int_name, "addr":interf.address}
|
||||
return list(_get_interfaces())
|
||||
|
||||
def nftables_int_to_json(ip_int):
|
||||
ip_int = ip_parse(ip_int)
|
||||
ip_addr = str(ip_int).split("/")[0]
|
||||
ip_addr_cidr = int(str(ip_int).split("/")[1])
|
||||
return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}
|
||||
|
||||
def nftables_json_to_int(ip_json_int):
|
||||
if isinstance(ip_json_int,str):
|
||||
return str(ip_parse(ip_json_int))
|
||||
else:
|
||||
return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}'
|
||||
|
||||
class Singleton(object):
|
||||
__instance = None
|
||||
def __new__(class_, *args, **kwargs):
|
||||
if not isinstance(class_.__instance, class_):
|
||||
class_.__instance = object.__new__(class_, *args, **kwargs)
|
||||
return class_.__instance
|
||||
|
||||
class NFTableManager(Singleton):
|
||||
|
||||
table_name = "firegex"
|
||||
|
||||
def __init__(self, init_cmd, reset_cmd):
|
||||
self.__init_cmds = init_cmd
|
||||
self.__reset_cmds = reset_cmd
|
||||
self.nft = nftables.Nftables()
|
||||
|
||||
def raw_cmd(self, *cmds):
|
||||
return self.nft.json_cmd({"nftables": list(cmds)})
|
||||
|
||||
def cmd(self, *cmds):
|
||||
code, out, err = self.raw_cmd(*cmds)
|
||||
if code == 0:
|
||||
return out
|
||||
else:
|
||||
raise Exception(err)
|
||||
|
||||
def init(self):
|
||||
self.reset()
|
||||
self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}})
|
||||
self.cmd(*self.__init_cmds)
|
||||
|
||||
def reset(self):
|
||||
self.raw_cmd(*self.__reset_cmds)
|
||||
|
||||
def list_rules(self, tables = None, chains = None):
|
||||
for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]:
|
||||
if tables and filter["table"] not in tables:
|
||||
continue
|
||||
if chains and filter["chain"] not in chains:
|
||||
continue
|
||||
yield filter
|
||||
|
||||
def raw_list(self):
|
||||
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"):
|
||||
res = obj.model_dump(mode=mode, exclude_unset=not unset)
|
||||
if convert_keys:
|
||||
for from_k, to_k in convert_keys.items():
|
||||
if from_k in res:
|
||||
res[to_k] = res.pop(from_k)
|
||||
if exclude:
|
||||
for ele in exclude:
|
||||
if ele in res:
|
||||
del res[ele]
|
||||
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:
|
||||
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(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 decorator(func):
|
||||
@sio_server.on(event_name) # Automatically registers the event
|
||||
@wraps(func)
|
||||
async def wrapper(sid, data):
|
||||
try:
|
||||
# Parse and validate incoming data
|
||||
parsed_data = model.model_validate(data)
|
||||
except ValidationError:
|
||||
return json_like(StatusMessageModel(status=f"Invalid {event_name} request"))
|
||||
|
||||
# Call the original function with the parsed data
|
||||
result = await func(sid, parsed_data)
|
||||
# If a response model is provided, validate the output
|
||||
if response_model:
|
||||
try:
|
||||
parsed_result = response_model.model_validate(result)
|
||||
except ValidationError:
|
||||
traceback.print_exc()
|
||||
return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response"))
|
||||
else:
|
||||
parsed_result = result
|
||||
# Emit the validated result
|
||||
if parsed_result:
|
||||
if isinstance(parsed_result, BaseModel):
|
||||
return json_like(parsed_result)
|
||||
return parsed_result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def nicenessify(priority:int, pid:int|None=None):
|
||||
try:
|
||||
pid = os.getpid() if pid is None else pid
|
||||
ps = psutil.Process(pid)
|
||||
if os.name == 'posix':
|
||||
ps.nice(priority)
|
||||
except Exception as e:
|
||||
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
||||
pass
|
||||
|
||||
116
docs/TRAFFIC_VIEWER.md
Normal file
116
docs/TRAFFIC_VIEWER.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Traffic Viewer - JSON Event Format
|
||||
|
||||
The traffic viewer is now fully integrated and supports **TCP, HTTP, and UDP** protocols. 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
|
||||
@@ -10,7 +10,7 @@ from firegex.nfproxy.internals.exceptions import (
|
||||
from firegex.nfproxy.internals.models import FullStreamAction, ExceptionAction
|
||||
from dataclasses import dataclass, field
|
||||
from collections import deque
|
||||
from compression import zstd
|
||||
import zstandard as zstd
|
||||
import gzip
|
||||
import io
|
||||
import zlib
|
||||
|
||||
@@ -13,6 +13,9 @@ import { Firewall } from './pages/Firewall';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import NFProxy from './pages/NFProxy';
|
||||
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
||||
import TrafficViewer from './pages/NFProxy/TrafficViewer';
|
||||
import TrafficViewerMain from './pages/TrafficViewer';
|
||||
import SetupPage from './pages/Setup';
|
||||
import { useAuthStore } from './js/store';
|
||||
|
||||
function App() {
|
||||
@@ -172,9 +175,12 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
|
||||
</Route>
|
||||
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
|
||||
<Route path=":srv" element={<ServiceDetailsNFProxy />} />
|
||||
<Route path=":srv/traffic" element={<TrafficViewer />} />
|
||||
</Route>
|
||||
<Route path="traffic" element={<TrafficViewerMain />} />
|
||||
<Route path="firewall" element={<Firewall />} />
|
||||
<Route path="porthijack" element={<PortHijack />} />
|
||||
<Route path="setup" element={<SetupPage />} />
|
||||
<Route path="*" element={<HomeRedirector />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useState } from 'react';
|
||||
import { RegexAddForm } from '../js/models';
|
||||
import { b64decode, b64encode, okNotify } from '../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex } from './NFRegex/utils';
|
||||
|
||||
type RegexAddInfo = {
|
||||
regex:string,
|
||||
mode:string,
|
||||
is_case_insensitive:boolean,
|
||||
deactive:boolean
|
||||
}
|
||||
|
||||
function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:string }) {
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
regex:"",
|
||||
mode:"C",
|
||||
is_case_insensitive:false,
|
||||
deactive:false
|
||||
},
|
||||
validate:{
|
||||
regex: (value) => value !== "" ? null : "Regex is required",
|
||||
mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode",
|
||||
}
|
||||
})
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = (values:RegexAddInfo) => {
|
||||
setSubmitLoading(true)
|
||||
|
||||
const request:RegexAddForm = {
|
||||
is_case_sensitive: !values.is_case_insensitive,
|
||||
service_id: service,
|
||||
mode: values.mode?values.mode:"B",
|
||||
regex: b64encode(values.regex),
|
||||
active: !values.deactive
|
||||
}
|
||||
setSubmitLoading(false)
|
||||
nfregex.regexesadd(request).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`)
|
||||
}else if (res.toLowerCase() === "invalid regex"){
|
||||
setSubmitLoading(false)
|
||||
form.setFieldError("regex", "Invalid Regex")
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Error: [ "+res+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
<TextInput
|
||||
label="Regex"
|
||||
placeholder="[A-Z0-9]{31}="
|
||||
{...form.getInputProps('regex')}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Case insensitive"
|
||||
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Deactivate"
|
||||
{...form.getInputProps('deactive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Select
|
||||
data={[
|
||||
{ value: 'C', label: 'Client -> Server' },
|
||||
{ value: 'S', label: 'Server -> Client' },
|
||||
{ value: 'B', label: 'Both (Client <-> Server)' },
|
||||
]}
|
||||
label="Choose the source of the packets to filter"
|
||||
variant="filled"
|
||||
{...form.getInputProps('mode')}
|
||||
/>
|
||||
<Group align="right" mt="md">
|
||||
<Button loading={submitLoading} type="submit">Add Filter</Button>
|
||||
</Group>
|
||||
|
||||
<Space h="md" />
|
||||
|
||||
{error?<>
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" /></>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddNewRegex;
|
||||
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useState } from 'react';
|
||||
import { RegexAddForm } from '../js/models';
|
||||
import { b64decode, b64encode, okNotify } from '../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex } from './NFRegex/utils';
|
||||
|
||||
type RegexAddInfo = {
|
||||
regex:string,
|
||||
mode:string,
|
||||
is_case_insensitive:boolean,
|
||||
deactive:boolean
|
||||
}
|
||||
|
||||
function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:string }) {
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
regex:"",
|
||||
mode:"C",
|
||||
is_case_insensitive:false,
|
||||
deactive:false
|
||||
},
|
||||
validate:{
|
||||
regex: (value) => value !== "" ? null : "Regex is required",
|
||||
mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode",
|
||||
}
|
||||
})
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = (values:RegexAddInfo) => {
|
||||
setSubmitLoading(true)
|
||||
|
||||
const request:RegexAddForm = {
|
||||
is_case_sensitive: !values.is_case_insensitive,
|
||||
service_id: service,
|
||||
mode: values.mode?values.mode:"B",
|
||||
regex: b64encode(values.regex),
|
||||
active: !values.deactive
|
||||
}
|
||||
setSubmitLoading(false)
|
||||
nfregex.regexesadd(request).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`)
|
||||
}else if (res.toLowerCase() === "invalid regex"){
|
||||
setSubmitLoading(false)
|
||||
form.setFieldError("regex", "Invalid Regex")
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Error: [ "+res+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
<TextInput
|
||||
label="Regex"
|
||||
placeholder="[A-Z0-9]{31}="
|
||||
{...form.getInputProps('regex')}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Case insensitive"
|
||||
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Deactivate"
|
||||
{...form.getInputProps('deactive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Select
|
||||
data={[
|
||||
{ value: 'C', label: 'Client -> Server' },
|
||||
{ value: 'S', label: 'Server -> Client' },
|
||||
{ value: 'B', label: 'Both (Client <-> Server)' },
|
||||
]}
|
||||
label="Choose the source of the packets to filter"
|
||||
variant="filled"
|
||||
{...form.getInputProps('mode')}
|
||||
/>
|
||||
<Group align="right" mt="md">
|
||||
<Button loading={submitLoading} type="submit">Add Filter</Button>
|
||||
</Group>
|
||||
|
||||
<Space h="md" />
|
||||
|
||||
{error?<>
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" /></>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddNewRegex;
|
||||
|
||||
@@ -1,82 +1,82 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core';
|
||||
import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils';
|
||||
import { AiFillHome } from "react-icons/ai"
|
||||
import { useNavigate } from 'react-router';
|
||||
import { FaLock } from 'react-icons/fa';
|
||||
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
|
||||
import { ImExit } from 'react-icons/im';
|
||||
import ResetPasswordModal from './ResetPasswordModal';
|
||||
import ResetModal from './ResetModal';
|
||||
import { MenuDropDownWithButton } from '../MainLayout';
|
||||
import { useNavbarStore } from '../../js/store';
|
||||
|
||||
|
||||
function HeaderPage(props: any) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const { navOpened, toggleNav } = useNavbarStore()
|
||||
|
||||
const logout_action = () => {
|
||||
logout().then(r => {
|
||||
window.location.reload()
|
||||
}).catch(r => {
|
||||
errorNotify("Logout failed!",`Error: ${r}`)
|
||||
})
|
||||
}
|
||||
|
||||
const go_to_home = () => {
|
||||
navigator(`/${getMainPath()}`)
|
||||
}
|
||||
|
||||
const [changePasswordModal, setChangePasswordModal] = useState(false);
|
||||
const [resetFiregexModal, setResetFiregexModal] = useState(false);
|
||||
return <AppShell.Header className="firegex__header__header" {...props}>
|
||||
<Burger
|
||||
hiddenFrom='md'
|
||||
ml="lg"
|
||||
opened={navOpened}
|
||||
className="firegex__header__navbtn"
|
||||
onClick={toggleNav}
|
||||
size="sm"
|
||||
/>
|
||||
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center"}} ml={5}>
|
||||
<Box className="firegex__header__divlogo">
|
||||
<Tooltip zIndex={0} label="Home" openDelay={1000} color="dark" position="right" >
|
||||
<Image src="/header-logo.png" alt="Firegex logo" w={50} onClick={()=>navigator("/")}/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
|
||||
<Title order={2} >[Fi]*regex</Title>
|
||||
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a></p>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="flex-spacer" />
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Label>Firewall Access</Menu.Label>
|
||||
<Menu.Item leftSection={<FaLock size={14} />} onClick={() => setChangePasswordModal(true)}>Change Password</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label>Actions</Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md" />
|
||||
<Tooltip label="Home" position='bottom' color="teal">
|
||||
<ActionIcon color="teal" style={{marginRight:"10px"}}
|
||||
size="xl" radius="md" variant="filled"
|
||||
onClick={go_to_home}>
|
||||
<AiFillHome size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Logout" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled">
|
||||
<ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
|
||||
</Tooltip>
|
||||
<ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} />
|
||||
<ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} />
|
||||
<Space w="xl" />
|
||||
</AppShell.Header>
|
||||
}
|
||||
|
||||
export default HeaderPage;
|
||||
import React, { useState } from 'react';
|
||||
import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core';
|
||||
import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils';
|
||||
import { AiFillHome } from "react-icons/ai"
|
||||
import { useNavigate } from 'react-router';
|
||||
import { FaLock } from 'react-icons/fa';
|
||||
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
|
||||
import { ImExit } from 'react-icons/im';
|
||||
import ResetPasswordModal from './ResetPasswordModal';
|
||||
import ResetModal from './ResetModal';
|
||||
import { MenuDropDownWithButton } from '../MainLayout';
|
||||
import { useNavbarStore } from '../../js/store';
|
||||
|
||||
|
||||
function HeaderPage(props: any) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const { navOpened, toggleNav } = useNavbarStore()
|
||||
|
||||
const logout_action = () => {
|
||||
logout().then(r => {
|
||||
window.location.reload()
|
||||
}).catch(r => {
|
||||
errorNotify("Logout failed!",`Error: ${r}`)
|
||||
})
|
||||
}
|
||||
|
||||
const go_to_home = () => {
|
||||
navigator(`/${getMainPath()}`)
|
||||
}
|
||||
|
||||
const [changePasswordModal, setChangePasswordModal] = useState(false);
|
||||
const [resetFiregexModal, setResetFiregexModal] = useState(false);
|
||||
return <AppShell.Header className="firegex__header__header" {...props}>
|
||||
<Burger
|
||||
hiddenFrom='md'
|
||||
ml="lg"
|
||||
opened={navOpened}
|
||||
className="firegex__header__navbtn"
|
||||
onClick={toggleNav}
|
||||
size="sm"
|
||||
/>
|
||||
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center"}} ml={5}>
|
||||
<Box className="firegex__header__divlogo">
|
||||
<Tooltip zIndex={0} label="Home" openDelay={1000} color="dark" position="right" >
|
||||
<Image src="/header-logo.png" alt="Firegex logo" w={50} onClick={()=>navigator("/")}/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
|
||||
<Title order={2} >[Fi]*regex</Title>
|
||||
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a></p>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="flex-spacer" />
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Label>Firewall Access</Menu.Label>
|
||||
<Menu.Item leftSection={<FaLock size={14} />} onClick={() => setChangePasswordModal(true)}>Change Password</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label>Actions</Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md" />
|
||||
<Tooltip label="Home" position='bottom' color="teal">
|
||||
<ActionIcon color="teal" style={{marginRight:"10px"}}
|
||||
size="xl" radius="md" variant="filled"
|
||||
onClick={go_to_home}>
|
||||
<AiFillHome size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Logout" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled">
|
||||
<ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
|
||||
</Tooltip>
|
||||
<ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} />
|
||||
<ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} />
|
||||
<Space w="xl" />
|
||||
</AppShell.Header>
|
||||
}
|
||||
|
||||
export default HeaderPage;
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
import { useEffect } from 'react';
|
||||
import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core';
|
||||
import { AppShell } from '@mantine/core';
|
||||
import NavBar from './NavBar';
|
||||
import HeaderPage from './Header';
|
||||
import { getMainPath } from '../js/utils';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useNavbarStore } from '../js/store';
|
||||
import { HiMenu } from "react-icons/hi";
|
||||
|
||||
|
||||
function MainLayout({ children }:{ children:any }) {
|
||||
const { navOpened } = useNavbarStore()
|
||||
const location = useLocation()
|
||||
useEffect(()=>{
|
||||
if (location.pathname !== "/"){
|
||||
sessionStorage.setItem('home_section', getMainPath())
|
||||
}
|
||||
},[location.pathname])
|
||||
return <AppShell
|
||||
header={{ height: 70 }}
|
||||
navbar={{ width: 300 , breakpoint: "md", collapsed: { mobile: !navOpened } }}
|
||||
p="md"
|
||||
>
|
||||
<HeaderPage />
|
||||
<NavBar />
|
||||
<AppShell.Main>
|
||||
<Container size="lg">
|
||||
{children}
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
<Space h="lg" />
|
||||
|
||||
</AppShell>
|
||||
|
||||
}
|
||||
|
||||
export default MainLayout;
|
||||
|
||||
export const MenuDropDownWithButton = ({children}:{children:any}) => <Menu withArrow>
|
||||
<Menu.Target>
|
||||
<Tooltip label="More options" color="gray">
|
||||
<ActionIcon variant='transparent'>
|
||||
<HiMenu size={24} color='#FFF'/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{children}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
import { useEffect } from 'react';
|
||||
import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core';
|
||||
import { AppShell } from '@mantine/core';
|
||||
import NavBar from './NavBar';
|
||||
import HeaderPage from './Header';
|
||||
import { getMainPath } from '../js/utils';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useNavbarStore } from '../js/store';
|
||||
import { HiMenu } from "react-icons/hi";
|
||||
|
||||
|
||||
function MainLayout({ children }:{ children:any }) {
|
||||
const { navOpened } = useNavbarStore()
|
||||
const location = useLocation()
|
||||
useEffect(()=>{
|
||||
if (location.pathname !== "/"){
|
||||
sessionStorage.setItem('home_section', getMainPath())
|
||||
}
|
||||
},[location.pathname])
|
||||
return <AppShell
|
||||
header={{ height: 70 }}
|
||||
navbar={{ width: 300 , breakpoint: "md", collapsed: { mobile: !navOpened } }}
|
||||
p="md"
|
||||
>
|
||||
<HeaderPage />
|
||||
<NavBar />
|
||||
<AppShell.Main>
|
||||
<Container size="lg">
|
||||
{children}
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
<Space h="lg" />
|
||||
|
||||
</AppShell>
|
||||
|
||||
}
|
||||
|
||||
export default MainLayout;
|
||||
|
||||
export const MenuDropDownWithButton = ({children}:{children:any}) => <Menu withArrow>
|
||||
<Menu.Target>
|
||||
<Tooltip label="More options" color="gray">
|
||||
<ActionIcon variant='transparent'>
|
||||
<HiMenu size={24} color='#FFF'/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{children}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@@ -1,139 +1,140 @@
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfproxy, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfproxy.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
{edit?null:<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>}
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfproxy, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","http","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfproxy.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
{edit?null:<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>}
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
|
||||
@@ -72,7 +72,7 @@ export const HELP_NFPROXY_SIM = `➤ fgex nfproxy -h
|
||||
│ * port INTEGER The port of the target to proxy [default: None] [required] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ --proto [tcp|http] The protocol to proxy [default: tcp] │
|
||||
│ --proto [tcp|http|udp] The protocol to proxy [default: tcp] │
|
||||
│ --from-address TEXT The address of the local server [default: None] │
|
||||
│ --from-port INTEGER The port of the local server [default: 7474] │
|
||||
│ -6 Use IPv6 for the connection │
|
||||
|
||||
@@ -1,164 +1,164 @@
|
||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfproxy, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected } from "react-icons/tb";
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
import { FaPencilAlt } from "react-icons/fa";
|
||||
import { ExceptionWarning } from '../ExceptionWarning';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<ExceptionWarning service_id={service.service_id} />
|
||||
<Space w="sm"/>
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfproxy, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected } from "react-icons/tb";
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
import { FaPencilAlt } from "react-icons/fa";
|
||||
import { ExceptionWarning } from '../ExceptionWarning';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<ExceptionWarning service_id={service.service_id} />
|
||||
<Space w="sm"/>
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,175 +1,182 @@
|
||||
import { PyFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
service_id:string,
|
||||
name:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_filters:number,
|
||||
edited_packets:number,
|
||||
blocked_packets:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfproxy","services"]
|
||||
|
||||
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
|
||||
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters"],
|
||||
queryFn:() => nfproxy.servicepyfilters(service_id)
|
||||
})
|
||||
|
||||
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
|
||||
queryFn:() => nfproxy.getpyfilterscode(service_id)
|
||||
})
|
||||
|
||||
export const nfproxy = {
|
||||
services: async () => {
|
||||
return await getapi("nfproxy/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}`) as Service;
|
||||
},
|
||||
pyfilterenable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
pyfilterdisable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfproxy/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicepyfilters: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
getpyfilterscode: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
||||
},
|
||||
setpyfilterscode: async (service_id:string, code:string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
|
||||
|
||||
# From here we can import the DataTypes that we want to use:
|
||||
# The data type must be specified in the filter functions
|
||||
# And will also interally be used to decide when call some filters and how aggregate data
|
||||
from firegex.nfproxy.models import RawPacket
|
||||
|
||||
# global context in this execution is dedicated to a single TCP stream
|
||||
# - This code will be executed once at the TCP stream start
|
||||
# - The filter will be called for each packet in the stream
|
||||
# - You can store in global context some data you need, but exceeding with data stored could be dangerous
|
||||
# - At the end of the stream the global context will be destroyed
|
||||
|
||||
from firegex.nfproxy import pyfilter
|
||||
# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
|
||||
|
||||
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
|
||||
# - The filter must return one of the following values:
|
||||
# - ACCEPT: The packet will be accepted
|
||||
# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream)
|
||||
# - UNSTABLE_MANGLE: The packet will be mangled and accepted
|
||||
# - DROP: All the packets in this stream will be easly dropped
|
||||
|
||||
# If you want, you can use print to debug your filters, but this could slow down the filter
|
||||
|
||||
# Filter names must be unique and are specified by the name of the function wrapped by the decorator
|
||||
@pyfilter
|
||||
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
|
||||
def strange_filter(packet:RawPacket):
|
||||
# Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
|
||||
# Also is not garanteed that l4_data is the same of the packet data:
|
||||
# packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
|
||||
# Unorder packets in TCP are accepted by default, and python is not called in this case
|
||||
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
|
||||
if b"TEST_MANGLING" in packet.l4_data:
|
||||
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
|
||||
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
|
||||
return UNSTABLE_MANGLE
|
||||
# Drops the traffic
|
||||
if b"BAD DATA 1" in packet.data:
|
||||
return DROP
|
||||
# Rejects the traffic
|
||||
if b"BAD DATA 2" in packet.data:
|
||||
return REJECT
|
||||
# Accepts the traffic (default if None is returned)
|
||||
return ACCEPT
|
||||
|
||||
# Example with a higher level of abstraction
|
||||
@pyfilter
|
||||
def http_filter(http:HTTPRequest):
|
||||
if http.method == "GET" and "test" in http.url:
|
||||
return REJECT
|
||||
|
||||
# ADVANCED OPTIONS
|
||||
# You can specify some additional options on the streaming managment
|
||||
# pyproxy will automatically store all the packets (already ordered by the c++ binary):
|
||||
#
|
||||
# If the stream is too big, you can specify what actions to take:
|
||||
# This can be done defining some variables in the global context
|
||||
# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB)
|
||||
# NOTE: the stream size is calculated and managed indipendently by the data type handling system
|
||||
# Only types required by at least 1 filter will be stored.
|
||||
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
|
||||
# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
|
||||
# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
|
||||
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
|
||||
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic
|
||||
|
||||
from firege.nfproxy import FullStreamAction
|
||||
|
||||
# Example of a global context
|
||||
FGEX_STREAM_MAX_SIZE = 4096
|
||||
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT
|
||||
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic
|
||||
`
|
||||
import { PyFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
service_id:string,
|
||||
name:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_filters:number,
|
||||
edited_packets:number,
|
||||
blocked_packets:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfproxy","services"]
|
||||
|
||||
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
|
||||
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters"],
|
||||
queryFn:() => nfproxy.servicepyfilters(service_id)
|
||||
})
|
||||
|
||||
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
|
||||
queryFn:() => nfproxy.getpyfilterscode(service_id)
|
||||
})
|
||||
|
||||
export const nfproxy = {
|
||||
services: async () => {
|
||||
return await getapi("nfproxy/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}`) as Service;
|
||||
},
|
||||
pyfilterenable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
pyfilterdisable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfproxy/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicepyfilters: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
getpyfilterscode: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
||||
},
|
||||
setpyfilterscode: async (service_id:string, code:string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
gettraffic: async (service_id:string, limit:number = 500) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/traffic?limit=${limit}`) as { events: any[], count: number };
|
||||
},
|
||||
cleartraffic: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/traffic/clear`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
|
||||
|
||||
# From here we can import the DataTypes that we want to use:
|
||||
# The data type must be specified in the filter functions
|
||||
# And will also interally be used to decide when call some filters and how aggregate data
|
||||
from firegex.nfproxy.models import RawPacket
|
||||
|
||||
# global context in this execution is dedicated to a single TCP stream
|
||||
# - This code will be executed once at the TCP stream start
|
||||
# - The filter will be called for each packet in the stream
|
||||
# - You can store in global context some data you need, but exceeding with data stored could be dangerous
|
||||
# - At the end of the stream the global context will be destroyed
|
||||
|
||||
from firegex.nfproxy import pyfilter
|
||||
# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
|
||||
|
||||
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
|
||||
# - The filter must return one of the following values:
|
||||
# - ACCEPT: The packet will be accepted
|
||||
# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream)
|
||||
# - UNSTABLE_MANGLE: The packet will be mangled and accepted
|
||||
# - DROP: All the packets in this stream will be easly dropped
|
||||
|
||||
# If you want, you can use print to debug your filters, but this could slow down the filter
|
||||
|
||||
# Filter names must be unique and are specified by the name of the function wrapped by the decorator
|
||||
@pyfilter
|
||||
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
|
||||
def strange_filter(packet:RawPacket):
|
||||
# Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
|
||||
# Also is not garanteed that l4_data is the same of the packet data:
|
||||
# packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
|
||||
# Unorder packets in TCP are accepted by default, and python is not called in this case
|
||||
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
|
||||
if b"TEST_MANGLING" in packet.l4_data:
|
||||
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
|
||||
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
|
||||
return UNSTABLE_MANGLE
|
||||
# Drops the traffic
|
||||
if b"BAD DATA 1" in packet.data:
|
||||
return DROP
|
||||
# Rejects the traffic
|
||||
if b"BAD DATA 2" in packet.data:
|
||||
return REJECT
|
||||
# Accepts the traffic (default if None is returned)
|
||||
return ACCEPT
|
||||
|
||||
# Example with a higher level of abstraction
|
||||
@pyfilter
|
||||
def http_filter(http:HTTPRequest):
|
||||
if http.method == "GET" and "test" in http.url:
|
||||
return REJECT
|
||||
|
||||
# ADVANCED OPTIONS
|
||||
# You can specify some additional options on the streaming managment
|
||||
# pyproxy will automatically store all the packets (already ordered by the c++ binary):
|
||||
#
|
||||
# If the stream is too big, you can specify what actions to take:
|
||||
# This can be done defining some variables in the global context
|
||||
# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB)
|
||||
# NOTE: the stream size is calculated and managed indipendently by the data type handling system
|
||||
# Only types required by at least 1 filter will be stored.
|
||||
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
|
||||
# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
|
||||
# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
|
||||
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
|
||||
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic
|
||||
|
||||
from firege.nfproxy import FullStreamAction
|
||||
|
||||
# Example of a global context
|
||||
FGEX_STREAM_MAX_SIZE = 4096
|
||||
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT
|
||||
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic
|
||||
`
|
||||
|
||||
@@ -1,139 +1,139 @@
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfregex.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfregex.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
|
||||
@@ -1,158 +1,158 @@
|
||||
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
import { RegexFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { RegexAddForm } from "../../js/models"
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
name:string,
|
||||
service_id:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_packets:number,
|
||||
n_regex:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
proto?:string,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfregex","services"]
|
||||
|
||||
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
|
||||
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"regexes"],
|
||||
queryFn:() => nfregex.serviceregexes(service_id)
|
||||
})
|
||||
|
||||
export const nfregex = {
|
||||
services: async () => {
|
||||
return await getapi("nfregex/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}`) as Service;
|
||||
},
|
||||
regexdelete: async (regex_id:number) => {
|
||||
const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexenable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexdisable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfregex/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexesadd: async (data:RegexAddForm) => {
|
||||
const { status } = await postapi("nfregex/regexes",data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
serviceregexes: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
import { RegexFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { RegexAddForm } from "../../js/models"
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
name:string,
|
||||
service_id:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_packets:number,
|
||||
n_regex:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
proto?:string,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfregex","services"]
|
||||
|
||||
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
|
||||
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"regexes"],
|
||||
queryFn:() => nfregex.serviceregexes(service_id)
|
||||
})
|
||||
|
||||
export const nfregex = {
|
||||
services: async () => {
|
||||
return await getapi("nfregex/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}`) as Service;
|
||||
},
|
||||
regexdelete: async (regex_id:number) => {
|
||||
const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexenable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexdisable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfregex/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexesadd: async (data:RegexAddForm) => {
|
||||
const { status } = await postapi("nfregex/regexes",data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
serviceregexes: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { PiWallLight } from "react-icons/pi";
|
||||
import { useNavbarStore } from "../../js/store";
|
||||
import { getMainPath } from "../../js/utils";
|
||||
import { BsRegex } from "react-icons/bs";
|
||||
import { MdVisibility, MdSettings } from "react-icons/md";
|
||||
|
||||
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
||||
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
|
||||
@@ -40,6 +41,8 @@ export default function NavBar() {
|
||||
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
|
||||
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
|
||||
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
||||
<NavBarButton navigate="traffic" closeNav={closeNav} name="Traffic Viewer" color="cyan" icon={<MdVisibility size={19} />} />
|
||||
<NavBarButton navigate="setup" closeNav={closeNav} name="Setup Import/Export" color="teal" icon={<MdSettings size={19} />} />
|
||||
{/* <Box px="xs" mt="lg">
|
||||
<Title order={5}>Experimental Features 🧪</Title>
|
||||
</Box>
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useState } from 'react';
|
||||
import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { porthijack } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
|
||||
type ServiceAddForm = {
|
||||
name:string,
|
||||
public_port:number,
|
||||
proxy_port:number,
|
||||
proto:string,
|
||||
ip_src:string,
|
||||
ip_dst:string,
|
||||
autostart: boolean,
|
||||
}
|
||||
|
||||
function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) {
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name:"",
|
||||
public_port:80,
|
||||
proxy_port:8080,
|
||||
proto:"tcp",
|
||||
ip_src:"",
|
||||
ip_dst:"127.0.0.1",
|
||||
autostart: false,
|
||||
},
|
||||
validate:{
|
||||
name: (value) => value !== ""? null : "Service name is required",
|
||||
public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port",
|
||||
proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port",
|
||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_src: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid source IP address",
|
||||
ip_dst: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid destination IP address",
|
||||
}
|
||||
})
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, proxy_port, public_port, autostart, proto, ip_src, ip_dst }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) porthijack.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service from port ${public_port} to ${proxy_port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" />
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>
|
||||
<Box className="flex-spacer" />
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit">Add Service</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddNewService;
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useState } from 'react';
|
||||
import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { porthijack } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
|
||||
type ServiceAddForm = {
|
||||
name:string,
|
||||
public_port:number,
|
||||
proxy_port:number,
|
||||
proto:string,
|
||||
ip_src:string,
|
||||
ip_dst:string,
|
||||
autostart: boolean,
|
||||
}
|
||||
|
||||
function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) {
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name:"",
|
||||
public_port:80,
|
||||
proxy_port:8080,
|
||||
proto:"tcp",
|
||||
ip_src:"",
|
||||
ip_dst:"127.0.0.1",
|
||||
autostart: false,
|
||||
},
|
||||
validate:{
|
||||
name: (value) => value !== ""? null : "Service name is required",
|
||||
public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port",
|
||||
proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port",
|
||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_src: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid source IP address",
|
||||
ip_dst: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid destination IP address",
|
||||
}
|
||||
})
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, proxy_port, public_port, autostart, proto, ip_src, ip_dst }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) porthijack.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service from port ${public_port} to ${proxy_port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" />
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>
|
||||
<Box className="flex-spacer" />
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit">Add Service</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddNewService;
|
||||
|
||||
@@ -1,152 +1,152 @@
|
||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { porthijack, Service } from '../utils';
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils';
|
||||
import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import ChangeDestination from './ChangeDestination';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { MdDoubleArrow } from "react-icons/md";
|
||||
|
||||
export default function ServiceRow({ service }:{ service:Service }) {
|
||||
|
||||
let status_color = service.active ? "teal": "red"
|
||||
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [changeDestModal, setChangeDestModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const form = useForm({
|
||||
initialValues: { proxy_port:service.proxy_port },
|
||||
validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" }
|
||||
})
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await porthijack.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.public_port} has been stopped!`)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await porthijack.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.public_port} has been started!`)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
porthijack.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="md" variant="filled">{service.active?"ENABLED":"DISABLED"}</Badge>
|
||||
<Badge color={service.proto === "tcp"?"cyan":"orange"} radius="md" size="md" variant="filled">
|
||||
{service.proto}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color="lime" radius="sm" size="lg" variant="filled">
|
||||
FROM {service.ip_src} :{service.public_port}
|
||||
</Badge>
|
||||
<Space h="sm" />
|
||||
<Badge color="blue" radius="sm" size="lg" variant="filled">
|
||||
<Box className="center-flex">
|
||||
TO {service.ip_dst} :{service.proxy_port}
|
||||
</Box>
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Label><b>Rename service</b></Menu.Label>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Menu.Label><b>Change destination</b></Menu.Label>
|
||||
<Menu.Item leftSection={<BsArrowRepeat size={18} />} onClick={()=>setChangeDestModal(true)}>Change hijacking destination</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={!service.active}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={service.active}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.public_port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<ChangeDestination
|
||||
onClose={()=>setChangeDestModal(false)}
|
||||
opened={changeDestModal}
|
||||
service={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { porthijack, Service } from '../utils';
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils';
|
||||
import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import ChangeDestination from './ChangeDestination';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { MdDoubleArrow } from "react-icons/md";
|
||||
|
||||
export default function ServiceRow({ service }:{ service:Service }) {
|
||||
|
||||
let status_color = service.active ? "teal": "red"
|
||||
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [changeDestModal, setChangeDestModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const form = useForm({
|
||||
initialValues: { proxy_port:service.proxy_port },
|
||||
validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" }
|
||||
})
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await porthijack.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.public_port} has been stopped!`)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await porthijack.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.public_port} has been started!`)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
porthijack.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="md" variant="filled">{service.active?"ENABLED":"DISABLED"}</Badge>
|
||||
<Badge color={service.proto === "tcp"?"cyan":"orange"} radius="md" size="md" variant="filled">
|
||||
{service.proto}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color="lime" radius="sm" size="lg" variant="filled">
|
||||
FROM {service.ip_src} :{service.public_port}
|
||||
</Badge>
|
||||
<Space h="sm" />
|
||||
<Badge color="blue" radius="sm" size="lg" variant="filled">
|
||||
<Box className="center-flex">
|
||||
TO {service.ip_dst} :{service.proxy_port}
|
||||
</Box>
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Label><b>Rename service</b></Menu.Label>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Menu.Label><b>Change destination</b></Menu.Label>
|
||||
<Menu.Item leftSection={<BsArrowRepeat size={18} />} onClick={()=>setChangeDestModal(true)}>Change hijacking destination</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={!service.active}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={service.active}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.public_port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<ChangeDestination
|
||||
onClose={()=>setChangeDestModal(false)}
|
||||
opened={changeDestModal}
|
||||
service={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
import { ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
export type GeneralStats = {
|
||||
services:number
|
||||
}
|
||||
|
||||
export type Service = {
|
||||
name:string,
|
||||
service_id:string,
|
||||
active:boolean,
|
||||
proto: string,
|
||||
ip_src: string,
|
||||
ip_dst: string,
|
||||
proxy_port: number,
|
||||
public_port: number,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
public_port:number,
|
||||
proxy_port:number,
|
||||
proto:string,
|
||||
ip_src: string,
|
||||
ip_dst: string,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = ServerResponse & { service_id: string }
|
||||
|
||||
export const queryKey = ["porthijack","services"]
|
||||
|
||||
export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services})
|
||||
|
||||
export const porthijack = {
|
||||
services: async () : Promise<Service[]> => {
|
||||
return await getapi("porthijack/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`porthijack/services/${service_id}`) as Service;
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("porthijack/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => {
|
||||
return await putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse;
|
||||
}
|
||||
import { ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
export type GeneralStats = {
|
||||
services:number
|
||||
}
|
||||
|
||||
export type Service = {
|
||||
name:string,
|
||||
service_id:string,
|
||||
active:boolean,
|
||||
proto: string,
|
||||
ip_src: string,
|
||||
ip_dst: string,
|
||||
proxy_port: number,
|
||||
public_port: number,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
public_port:number,
|
||||
proxy_port:number,
|
||||
proto:string,
|
||||
ip_src: string,
|
||||
ip_dst: string,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = ServerResponse & { service_id: string }
|
||||
|
||||
export const queryKey = ["porthijack","services"]
|
||||
|
||||
export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services})
|
||||
|
||||
export const porthijack = {
|
||||
services: async () : Promise<Service[]> => {
|
||||
return await getapi("porthijack/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`porthijack/services/${service_id}`) as Service;
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("porthijack/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => {
|
||||
return await putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,44 @@
|
||||
import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { PyFilter } from '../../js/models';
|
||||
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||
import { FaPause, FaPlay } from 'react-icons/fa';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { nfproxy } from '../NFProxy/utils';
|
||||
import { FaPencilAlt } from 'react-icons/fa';
|
||||
|
||||
export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) {
|
||||
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const changeRegexStatus = () => {
|
||||
(filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`)
|
||||
}else{
|
||||
errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
return <Box my="sm" display="flex" style={{alignItems:"center"}}>
|
||||
|
||||
<Box className="firegex__regexview__pyfilter_text" style={{ width: "100%", alignItems: "center"}} display="flex" >
|
||||
<Badge size="sm" radius="lg" mr="sm" color={filterInfo.active?"lime":"red"} variant="filled" />
|
||||
{filterInfo.name}
|
||||
<Box className='flex-spacer' />
|
||||
<Space w="xs" />
|
||||
{isMedium?<>
|
||||
<Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge>
|
||||
<Space w="lg" />
|
||||
</>:null}
|
||||
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"}>
|
||||
<ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="lg" radius="md" variant="filled">
|
||||
{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
}
|
||||
import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { PyFilter } from '../../js/models';
|
||||
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||
import { FaPause, FaPlay } from 'react-icons/fa';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { nfproxy } from '../NFProxy/utils';
|
||||
import { FaPencilAlt } from 'react-icons/fa';
|
||||
|
||||
export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) {
|
||||
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const changeRegexStatus = () => {
|
||||
(filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`)
|
||||
}else{
|
||||
errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
return <Box my="sm" display="flex" style={{alignItems:"center"}}>
|
||||
|
||||
<Box className="firegex__regexview__pyfilter_text" style={{ width: "100%", alignItems: "center"}} display="flex" >
|
||||
<Badge size="sm" radius="lg" mr="sm" color={filterInfo.active?"lime":"red"} variant="filled" />
|
||||
{filterInfo.name}
|
||||
<Box className='flex-spacer' />
|
||||
<Space w="xs" />
|
||||
{isMedium?<>
|
||||
<Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge>
|
||||
<Space w="lg" />
|
||||
</>:null}
|
||||
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"}>
|
||||
<ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="lg" radius="md" variant="filled">
|
||||
{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
}
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { RegexFilter } from '../../js/models';
|
||||
import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||
import { BsTrashFill } from "react-icons/bs"
|
||||
import YesNoModal from '../YesNoModal';
|
||||
import { FaPause, FaPlay } from 'react-icons/fa';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
|
||||
import { nfregex } from '../NFRegex/utils';
|
||||
|
||||
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
|
||||
|
||||
const mode_string = regexInfo.mode === "C"? "C -> S":
|
||||
regexInfo.mode === "S"? "S -> C":
|
||||
regexInfo.mode === "B"? "C <-> S": "🤔"
|
||||
|
||||
let regex_expr = b64decode(regexInfo.regex);
|
||||
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
|
||||
const deleteRegex = () => {
|
||||
nfregex.regexdelete(regexInfo.id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`)
|
||||
}else{
|
||||
errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
const changeRegexStatus = () => {
|
||||
(regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`)
|
||||
}else{
|
||||
errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
return <Box className="firegex__regexview__box">
|
||||
<Box>
|
||||
<Box className='center-flex' style={{width: "100%"}}>
|
||||
<Box className="firegex__regexview__outer_regex_text">
|
||||
<Text className="firegex__regexview__regex_text" onClick={()=>{
|
||||
clipboard.copy(regex_expr)
|
||||
okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`)
|
||||
}}>{regex_expr}</Text>
|
||||
</Box>
|
||||
<Space w="xs" />
|
||||
<Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"}>
|
||||
<ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
|
||||
>{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Delete regex" zIndex={0} color="red" >
|
||||
<ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled">
|
||||
<BsTrashFill size={22} /></ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box display="flex" mt="sm" ml="xs">
|
||||
<Badge size="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {regexInfo.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color={regexInfo.active?"lime":"red"} variant="filled">{regexInfo.active?"ACTIVE":"DISABLED"}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color={regexInfo.is_case_sensitive?"grape":"pink"} variant="filled">{regexInfo.is_case_sensitive?"Strict":"Loose"}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color="blue" variant="filled">{mode_string}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this regex?'
|
||||
description={`You are going to delete the regex '${regex_expr}'.`}
|
||||
onClose={()=>setDeleteModal(false)}
|
||||
action={deleteRegex}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
|
||||
</Box>
|
||||
}
|
||||
|
||||
export default RegexView;
|
||||
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { RegexFilter } from '../../js/models';
|
||||
import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||
import { BsTrashFill } from "react-icons/bs"
|
||||
import YesNoModal from '../YesNoModal';
|
||||
import { FaPause, FaPlay } from 'react-icons/fa';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
|
||||
import { nfregex } from '../NFRegex/utils';
|
||||
|
||||
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
|
||||
|
||||
const mode_string = regexInfo.mode === "C"? "C -> S":
|
||||
regexInfo.mode === "S"? "S -> C":
|
||||
regexInfo.mode === "B"? "C <-> S": "🤔"
|
||||
|
||||
let regex_expr = b64decode(regexInfo.regex);
|
||||
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
|
||||
const deleteRegex = () => {
|
||||
nfregex.regexdelete(regexInfo.id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`)
|
||||
}else{
|
||||
errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
const changeRegexStatus = () => {
|
||||
(regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`)
|
||||
}else{
|
||||
errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
return <Box className="firegex__regexview__box">
|
||||
<Box>
|
||||
<Box className='center-flex' style={{width: "100%"}}>
|
||||
<Box className="firegex__regexview__outer_regex_text">
|
||||
<Text className="firegex__regexview__regex_text" onClick={()=>{
|
||||
clipboard.copy(regex_expr)
|
||||
okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`)
|
||||
}}>{regex_expr}</Text>
|
||||
</Box>
|
||||
<Space w="xs" />
|
||||
<Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"}>
|
||||
<ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
|
||||
>{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Delete regex" zIndex={0} color="red" >
|
||||
<ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled">
|
||||
<BsTrashFill size={22} /></ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box display="flex" mt="sm" ml="xs">
|
||||
<Badge size="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {regexInfo.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color={regexInfo.active?"lime":"red"} variant="filled">{regexInfo.active?"ACTIVE":"DISABLED"}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color={regexInfo.is_case_sensitive?"grape":"pink"} variant="filled">{regexInfo.is_case_sensitive?"Strict":"Loose"}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color="blue" variant="filled">{mode_string}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this regex?'
|
||||
description={`You are going to delete the regex '${regex_expr}'.`}
|
||||
onClose={()=>setDeleteModal(false)}
|
||||
action={deleteRegex}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
|
||||
</Box>
|
||||
}
|
||||
|
||||
export default RegexView;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Button, Group, Modal } from '@mantine/core';
|
||||
import React from 'react';
|
||||
|
||||
function YesNoModal( { title, description, action, onClose, opened}:{ title:string, description:string, onClose:()=>void, action:()=>void, opened:boolean} ){
|
||||
|
||||
return <Modal size="xl" title={title} opened={opened} onClose={onClose} centered>
|
||||
{description}
|
||||
<Group justify='flex-end' mt="md">
|
||||
<Button onClick={()=>{
|
||||
onClose()
|
||||
action()
|
||||
}} color="teal" type="submit">Yes</Button>
|
||||
<Button onClick={onClose} color="red" type="submit">No</Button>
|
||||
|
||||
</Group>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
import { Button, Group, Modal } from '@mantine/core';
|
||||
import React from 'react';
|
||||
|
||||
function YesNoModal( { title, description, action, onClose, opened}:{ title:string, description:string, onClose:()=>void, action:()=>void, opened:boolean} ){
|
||||
|
||||
return <Modal size="xl" title={title} opened={opened} onClose={onClose} centered>
|
||||
{description}
|
||||
<Group justify='flex-end' mt="md">
|
||||
<Button onClick={()=>{
|
||||
onClose()
|
||||
action()
|
||||
}} color="teal" type="submit">Yes</Button>
|
||||
<Button onClick={onClose} color="red" type="submit">No</Button>
|
||||
|
||||
</Group>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
export default YesNoModal;
|
||||
@@ -1,227 +1,227 @@
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
import { ImCross } from "react-icons/im";
|
||||
import { TiTick } from "react-icons/ti"
|
||||
import { Navigate } from "react-router";
|
||||
import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models";
|
||||
import { Buffer } from "buffer"
|
||||
import { QueryClient, useQuery } from "@tanstack/react-query";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { io } from "socket.io-client";
|
||||
import { useAuthStore, useSessionStore } from "./store";
|
||||
|
||||
export const IS_DEV = import.meta.env.DEV
|
||||
|
||||
export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$";
|
||||
export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$";
|
||||
export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$"
|
||||
export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
|
||||
export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$"
|
||||
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
|
||||
export const DEV_IP_BACKEND = "127.0.0.1:4444"
|
||||
|
||||
export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes
|
||||
|
||||
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
|
||||
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
|
||||
|
||||
export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
|
||||
return value as E;
|
||||
}
|
||||
|
||||
export const socketio = import.meta.env.DEV?
|
||||
io("ws://"+DEV_IP_BACKEND, {
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
}):
|
||||
io({
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
})
|
||||
|
||||
export const queryClient = new QueryClient({ defaultOptions: { queries: {
|
||||
staleTime: Infinity
|
||||
} }})
|
||||
|
||||
export function getErrorMessage(e: any) {
|
||||
let error = "Unknown error";
|
||||
if(typeof e == "string") return e
|
||||
if (e.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
error = e.response.data.error;
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
error = e.message || e.error;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") {
|
||||
if (e.status){
|
||||
return e.status
|
||||
}
|
||||
if (e.detail){
|
||||
if (typeof e.detail == "string")
|
||||
return e.detail
|
||||
if (e.detail[0] && e.detail[0].msg)
|
||||
return e.detail[0].msg
|
||||
}
|
||||
if (e.error){
|
||||
return e.error
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
|
||||
export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
|
||||
return await new Promise((resolve, reject) => {
|
||||
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
|
||||
method: method,
|
||||
credentials: "same-origin",
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}),
|
||||
"Authorization" : "Bearer " + useAuthStore.getState().getAccessToken()
|
||||
},
|
||||
body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined
|
||||
}).then(res => {
|
||||
if(res.status === 401) window.location.reload()
|
||||
if(res.status === 406) resolve({status:"Wrong Password"})
|
||||
if(!res.ok){
|
||||
const errorDefault = res.statusText
|
||||
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
|
||||
}
|
||||
res.text().then(t => {
|
||||
try{
|
||||
resolve(JSON.parse(t))
|
||||
}catch(e){
|
||||
resolve(t)
|
||||
}
|
||||
}).catch( err => reject(err))
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function getapi(path:string):Promise<any>{
|
||||
return await genericapi("GET",path)
|
||||
}
|
||||
|
||||
export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise<any>{
|
||||
return await genericapi("POST",path,data,is_form)
|
||||
}
|
||||
|
||||
export async function deleteapi(path:string):Promise<any>{
|
||||
return await genericapi("DELETE",path)
|
||||
}
|
||||
|
||||
export async function putapi(path:string,data:any):Promise<any>{
|
||||
return await genericapi("PUT",path,data)
|
||||
}
|
||||
|
||||
export function getMainPath(){
|
||||
const paths = window.location.pathname.split("/")
|
||||
if (paths.length > 1) return paths[1]
|
||||
return ""
|
||||
}
|
||||
|
||||
export function HomeRedirector(){
|
||||
const section = useSessionStore.getState().getHomeSection();
|
||||
const path = section?`/${section}`:`/nfregex`
|
||||
return <Navigate to={path} replace/>
|
||||
}
|
||||
|
||||
export async function resetfiregex(delete_data:boolean = false){
|
||||
const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse;
|
||||
return (status === "ok"?undefined:status)
|
||||
}
|
||||
|
||||
export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces)
|
||||
|
||||
export async function getipinterfaces(){
|
||||
return await getapi("interfaces") as IpInterface[];
|
||||
}
|
||||
|
||||
export async function getstatus(){
|
||||
return await getapi(`status`) as ServerStatusResponse;
|
||||
}
|
||||
|
||||
export async function logout(){
|
||||
useAuthStore.getState().clearAccessToken();
|
||||
}
|
||||
|
||||
export async function setpassword(data:PasswordSend) {
|
||||
const { status, access_token } = await postapi("set-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function changepassword(data:ChangePassword) {
|
||||
const { status, access_token } = await postapi("change-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function login(data:PasswordSend) {
|
||||
const from = {username: "login", password: data.password};
|
||||
const { status, access_token } = await postapi("login",from,true) as LoginResponse;
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status;
|
||||
}
|
||||
|
||||
export function errorNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'red',
|
||||
icon: <ImCross />,
|
||||
});
|
||||
}
|
||||
|
||||
export function okNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'teal',
|
||||
icon: <TiTick />,
|
||||
});
|
||||
}
|
||||
|
||||
export const makeid = (length:number) => {
|
||||
let result = '';
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const charactersLength = characters.length;
|
||||
let counter = 0;
|
||||
while (counter < length) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
counter += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function b64encode(data:number[]|string){
|
||||
return Buffer.from(data).toString('base64')
|
||||
}
|
||||
|
||||
export function b64decode(regexB64:string){
|
||||
return Buffer.from(regexB64, "base64").toString()
|
||||
}
|
||||
|
||||
export function isMediumScreen(){
|
||||
return useMediaQuery('(min-width: 600px)');
|
||||
}
|
||||
|
||||
export function isLargeScreen(){
|
||||
return useMediaQuery('(min-width: 992px)');
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
import { ImCross } from "react-icons/im";
|
||||
import { TiTick } from "react-icons/ti"
|
||||
import { Navigate } from "react-router";
|
||||
import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models";
|
||||
import { Buffer } from "buffer"
|
||||
import { QueryClient, useQuery } from "@tanstack/react-query";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { io } from "socket.io-client";
|
||||
import { useAuthStore, useSessionStore } from "./store";
|
||||
|
||||
export const IS_DEV = import.meta.env.DEV
|
||||
|
||||
export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$";
|
||||
export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$";
|
||||
export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$"
|
||||
export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
|
||||
export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$"
|
||||
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
|
||||
export const DEV_IP_BACKEND = "127.0.0.1:4444"
|
||||
|
||||
export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes
|
||||
|
||||
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
|
||||
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
|
||||
|
||||
export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
|
||||
return value as E;
|
||||
}
|
||||
|
||||
export const socketio = import.meta.env.DEV?
|
||||
io("ws://"+DEV_IP_BACKEND, {
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
}):
|
||||
io({
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
})
|
||||
|
||||
export const queryClient = new QueryClient({ defaultOptions: { queries: {
|
||||
staleTime: Infinity
|
||||
} }})
|
||||
|
||||
export function getErrorMessage(e: any) {
|
||||
let error = "Unknown error";
|
||||
if(typeof e == "string") return e
|
||||
if (e.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
error = e.response.data.error;
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
error = e.message || e.error;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") {
|
||||
if (e.status){
|
||||
return e.status
|
||||
}
|
||||
if (e.detail){
|
||||
if (typeof e.detail == "string")
|
||||
return e.detail
|
||||
if (e.detail[0] && e.detail[0].msg)
|
||||
return e.detail[0].msg
|
||||
}
|
||||
if (e.error){
|
||||
return e.error
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
|
||||
export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
|
||||
return await new Promise((resolve, reject) => {
|
||||
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
|
||||
method: method,
|
||||
credentials: "same-origin",
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}),
|
||||
"Authorization" : "Bearer " + useAuthStore.getState().getAccessToken()
|
||||
},
|
||||
body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined
|
||||
}).then(res => {
|
||||
if(res.status === 401) window.location.reload()
|
||||
if(res.status === 406) resolve({status:"Wrong Password"})
|
||||
if(!res.ok){
|
||||
const errorDefault = res.statusText
|
||||
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
|
||||
}
|
||||
res.text().then(t => {
|
||||
try{
|
||||
resolve(JSON.parse(t))
|
||||
}catch(e){
|
||||
resolve(t)
|
||||
}
|
||||
}).catch( err => reject(err))
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function getapi(path:string):Promise<any>{
|
||||
return await genericapi("GET",path)
|
||||
}
|
||||
|
||||
export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise<any>{
|
||||
return await genericapi("POST",path,data,is_form)
|
||||
}
|
||||
|
||||
export async function deleteapi(path:string):Promise<any>{
|
||||
return await genericapi("DELETE",path)
|
||||
}
|
||||
|
||||
export async function putapi(path:string,data:any):Promise<any>{
|
||||
return await genericapi("PUT",path,data)
|
||||
}
|
||||
|
||||
export function getMainPath(){
|
||||
const paths = window.location.pathname.split("/")
|
||||
if (paths.length > 1) return paths[1]
|
||||
return ""
|
||||
}
|
||||
|
||||
export function HomeRedirector(){
|
||||
const section = useSessionStore.getState().getHomeSection();
|
||||
const path = section?`/${section}`:`/nfregex`
|
||||
return <Navigate to={path} replace/>
|
||||
}
|
||||
|
||||
export async function resetfiregex(delete_data:boolean = false){
|
||||
const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse;
|
||||
return (status === "ok"?undefined:status)
|
||||
}
|
||||
|
||||
export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces)
|
||||
|
||||
export async function getipinterfaces(){
|
||||
return await getapi("interfaces") as IpInterface[];
|
||||
}
|
||||
|
||||
export async function getstatus(){
|
||||
return await getapi(`status`) as ServerStatusResponse;
|
||||
}
|
||||
|
||||
export async function logout(){
|
||||
useAuthStore.getState().clearAccessToken();
|
||||
}
|
||||
|
||||
export async function setpassword(data:PasswordSend) {
|
||||
const { status, access_token } = await postapi("set-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function changepassword(data:ChangePassword) {
|
||||
const { status, access_token } = await postapi("change-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function login(data:PasswordSend) {
|
||||
const from = {username: "login", password: data.password};
|
||||
const { status, access_token } = await postapi("login",from,true) as LoginResponse;
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status;
|
||||
}
|
||||
|
||||
export function errorNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'red',
|
||||
icon: <ImCross />,
|
||||
});
|
||||
}
|
||||
|
||||
export function okNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'teal',
|
||||
icon: <TiTick />,
|
||||
});
|
||||
}
|
||||
|
||||
export const makeid = (length:number) => {
|
||||
let result = '';
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const charactersLength = characters.length;
|
||||
let counter = 0;
|
||||
while (counter < length) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
counter += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function b64encode(data:number[]|string){
|
||||
return Buffer.from(data).toString('base64')
|
||||
}
|
||||
|
||||
export function b64decode(regexB64:string){
|
||||
return Buffer.from(regexB64, "base64").toString()
|
||||
}
|
||||
|
||||
export function isMediumScreen(){
|
||||
return useMediaQuery('(min-width: 600px)');
|
||||
}
|
||||
|
||||
export function isLargeScreen(){
|
||||
return useMediaQuery('(min-width: 992px)');
|
||||
}
|
||||
@@ -1,237 +1,243 @@
|
||||
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import PyFilterView from '../../components/PyFilterView';
|
||||
import { TbPlugConnected } from 'react-icons/tb';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { FaPython } from "react-icons/fa";
|
||||
import { FiFileText } from "react-icons/fi";
|
||||
import { ModalLog } from '../../components/ModalLog';
|
||||
import { useListState } from '@mantine/hooks';
|
||||
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
export default function ServiceDetailsNFProxy() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const services = nfproxyServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
const [openLogModal, setOpenLogModal] = useState(false)
|
||||
const [logData, logDataSetters] = useListState<string>([]);
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
if (srv){
|
||||
if (openLogModal){
|
||||
logDataSetters.setState([])
|
||||
socketio.emit("nfproxy-outstream-join", { service: srv });
|
||||
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
|
||||
logDataSetters.append(data)
|
||||
});
|
||||
}else{
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
return () => {
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
}
|
||||
}, [openLogModal, srv])
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={filtersList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<ExceptionWarning service_id={srv} />
|
||||
<Space w="sm" />
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Show logs" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
|
||||
<FiFileText size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider my="xl" />
|
||||
|
||||
{filterCode.data?<>
|
||||
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
|
||||
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
|
||||
</>: null}
|
||||
|
||||
{(!filtersList.data || filtersList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
|
||||
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
|
||||
}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
<ModalLog
|
||||
opened={openLogModal}
|
||||
close={()=>setOpenLogModal(false)}
|
||||
title={`Logs for service ${serviceInfo.name}`}
|
||||
data={logData.join("")}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import PyFilterView from '../../components/PyFilterView';
|
||||
import { TbPlugConnected } from 'react-icons/tb';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { FaPython } from "react-icons/fa";
|
||||
import { FiFileText } from "react-icons/fi";
|
||||
import { ModalLog } from '../../components/ModalLog';
|
||||
import { useListState } from '@mantine/hooks';
|
||||
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
export default function ServiceDetailsNFProxy() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const services = nfproxyServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
const [openLogModal, setOpenLogModal] = useState(false)
|
||||
const [logData, logDataSetters] = useListState<string>([]);
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
if (srv){
|
||||
if (openLogModal){
|
||||
logDataSetters.setState([])
|
||||
socketio.emit("nfproxy-outstream-join", { service: srv });
|
||||
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
|
||||
logDataSetters.append(data)
|
||||
});
|
||||
}else{
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
return () => {
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
}
|
||||
}, [openLogModal, srv])
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={filtersList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<ExceptionWarning service_id={srv} />
|
||||
<Space w="sm" />
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Show logs" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
|
||||
<FiFileText size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs"/>
|
||||
<Tooltip label="Traffic viewer" zIndex={0} color="grape">
|
||||
<ActionIcon color="grape" size="lg" radius="md" onClick={()=>navigate(`/nfproxy/${srv}/traffic`)} variant="filled">
|
||||
<MdDoubleArrow size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider my="xl" />
|
||||
|
||||
{filterCode.data?<>
|
||||
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
|
||||
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
|
||||
</>: null}
|
||||
|
||||
{(!filtersList.data || filtersList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
|
||||
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
|
||||
}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
<ModalLog
|
||||
opened={openLogModal}
|
||||
close={()=>setOpenLogModal(false)}
|
||||
title={`Logs for service ${serviceInfo.name}`}
|
||||
data={logData.join("")}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
306
frontend/src/pages/NFProxy/TrafficViewer.tsx
Normal file
306
frontend/src/pages/NFProxy/TrafficViewer.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import { ActionIcon, Badge, Box, Code, Divider, Grid, Group, LoadingOverlay, Modal, ScrollArea, Select, 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 [filterDirection, setFilterDirection] = useState<string | null>(null);
|
||||
const [filterProto, setFilterProto] = useState<string | null>(null);
|
||||
const [filterVerdict, setFilterVerdict] = useState<string | null>(null);
|
||||
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) => {
|
||||
// Text filter
|
||||
if (filterText) {
|
||||
const search = filterText.toLowerCase();
|
||||
const matchesText = (
|
||||
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) ||
|
||||
e.src_port?.toString().includes(search) ||
|
||||
e.dst_port?.toString().includes(search)
|
||||
);
|
||||
if (!matchesText) return false;
|
||||
}
|
||||
|
||||
// Direction filter
|
||||
if (filterDirection && e.direction !== filterDirection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Protocol filter
|
||||
if (filterProto && e.proto !== filterProto) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verdict filter
|
||||
if (filterVerdict && e.verdict !== filterVerdict) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
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">
|
||||
<Grid mb="md">
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<TextInput
|
||||
placeholder="Search by IP, port, verdict, filter name, or protocol..."
|
||||
value={filterText}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
|
||||
leftSection={<FaFilter />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 2 }}>
|
||||
<Select
|
||||
placeholder="Direction"
|
||||
clearable
|
||||
value={filterDirection}
|
||||
onChange={setFilterDirection}
|
||||
data={[
|
||||
{ value: 'in', label: 'Incoming' },
|
||||
{ value: 'out', label: 'Outgoing' }
|
||||
]}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 2 }}>
|
||||
<Select
|
||||
placeholder="Protocol"
|
||||
clearable
|
||||
value={filterProto}
|
||||
onChange={setFilterProto}
|
||||
data={[
|
||||
{ value: 'tcp', label: 'TCP' },
|
||||
{ value: 'udp', label: 'UDP' },
|
||||
{ value: 'http', label: 'HTTP' }
|
||||
]}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 2 }}>
|
||||
<Select
|
||||
placeholder="Verdict"
|
||||
clearable
|
||||
value={filterVerdict}
|
||||
onChange={setFilterVerdict}
|
||||
data={[
|
||||
{ value: 'accept', label: 'Accept' },
|
||||
{ value: 'drop', label: 'Drop' },
|
||||
{ value: 'reject', label: 'Reject' },
|
||||
{ value: 'edited', label: 'Edited' }
|
||||
]}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<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>
|
||||
</>;
|
||||
}
|
||||
@@ -1,172 +1,172 @@
|
||||
import { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFProxy/ServiceRow';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected, TbReload } from 'react-icons/tb';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
|
||||
import { MdUploadFile } from "react-icons/md";
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useFileDialog } from '@mantine/hooks';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
export default function NFProxy({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfproxyServiceQuery()
|
||||
const fileDialog = useFileDialog({
|
||||
accept: ".py",
|
||||
multiple: false,
|
||||
resetOnOpen: true,
|
||||
onChange: (files) => {
|
||||
if (files?.length??0 > 0)
|
||||
setFile(files![0])
|
||||
}
|
||||
});
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
useEffect(() => {
|
||||
if (!srv) return
|
||||
const service = services.data?.find(s => s.service_id === srv)
|
||||
if (!service) return
|
||||
if (file){
|
||||
console.log("Uploading code")
|
||||
const notify_id = notifications.show(
|
||||
{
|
||||
title: "Uploading code",
|
||||
message: `Uploading code for service ${service.name}`,
|
||||
color: "blue",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: false,
|
||||
loading: true,
|
||||
}
|
||||
)
|
||||
file.text()
|
||||
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
|
||||
.then( res => {
|
||||
if (!res){
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code uploaded",
|
||||
message: `Successfully uploaded code for service ${service.name}`,
|
||||
color: "green",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}else{
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${res}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
}).catch( err => {
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${err}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}).finally(()=>{setFile(null)})
|
||||
}
|
||||
}, [file])
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
|
||||
<MdUploadFile size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
|
||||
<BsPlusLg size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
|
||||
<TbReload size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfproxy" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfproxy/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter proxy is a simulated proxy written using python with a c++ core</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Then you can create a new service and write custom filters for the service</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfproxy" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{!srv?
|
||||
<AddEditService opened={open} onClose={closeModal} />:null
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
import { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFProxy/ServiceRow';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected, TbReload } from 'react-icons/tb';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
|
||||
import { MdUploadFile } from "react-icons/md";
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useFileDialog } from '@mantine/hooks';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
export default function NFProxy({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfproxyServiceQuery()
|
||||
const fileDialog = useFileDialog({
|
||||
accept: ".py",
|
||||
multiple: false,
|
||||
resetOnOpen: true,
|
||||
onChange: (files) => {
|
||||
if (files?.length??0 > 0)
|
||||
setFile(files![0])
|
||||
}
|
||||
});
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
useEffect(() => {
|
||||
if (!srv) return
|
||||
const service = services.data?.find(s => s.service_id === srv)
|
||||
if (!service) return
|
||||
if (file){
|
||||
console.log("Uploading code")
|
||||
const notify_id = notifications.show(
|
||||
{
|
||||
title: "Uploading code",
|
||||
message: `Uploading code for service ${service.name}`,
|
||||
color: "blue",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: false,
|
||||
loading: true,
|
||||
}
|
||||
)
|
||||
file.text()
|
||||
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
|
||||
.then( res => {
|
||||
if (!res){
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code uploaded",
|
||||
message: `Successfully uploaded code for service ${service.name}`,
|
||||
color: "green",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}else{
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${res}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
}).catch( err => {
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${err}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}).finally(()=>{setFile(null)})
|
||||
}
|
||||
}, [file])
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
|
||||
<MdUploadFile size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
|
||||
<BsPlusLg size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
|
||||
<TbReload size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfproxy" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfproxy/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter proxy is a simulated proxy written using python with a c++ core</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Then you can create a new service and write custom filters for the service</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfproxy" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{!srv?
|
||||
<AddEditService opened={open} onClose={closeModal} />:null
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,194 +1,194 @@
|
||||
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import RegexView from '../../components/RegexView';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { VscRegex } from 'react-icons/vsc';
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
|
||||
export default function ServiceDetailsNFRegex() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
const services = nfregexServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const regexesList = nfregexServiceRegexesQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={regexesList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider my="xl" />
|
||||
{(!regexesList.data || regexesList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
|
||||
<Space h="xl" /> <Space h="xl" />
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Add a new regex" zIndex={0} color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</>:
|
||||
<Grid>
|
||||
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import RegexView from '../../components/RegexView';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { VscRegex } from 'react-icons/vsc';
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
|
||||
export default function ServiceDetailsNFRegex() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
const services = nfregexServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const regexesList = nfregexServiceRegexesQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={regexesList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider my="xl" />
|
||||
{(!regexesList.data || regexesList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
|
||||
<Space h="xl" /> <Space h="xl" />
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Add a new regex" zIndex={0} color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</>:
|
||||
<Grid>
|
||||
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,100 +1,100 @@
|
||||
import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg, BsRegex } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFRegex/ServiceRow';
|
||||
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaFilter } from 'react-icons/fa';
|
||||
import { FaServer } from "react-icons/fa6";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
function NFRegex({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfregexServiceQuery()
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFRegex Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Add a new regex" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfregex" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfregex/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter Regex allows you to filter traffic using regexes</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start a service, add your regexes and it's already done!</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfregex" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{srv?
|
||||
<AddNewRegex opened={open} onClose={closeModal} service={srv} />:
|
||||
<AddEditService opened={open} onClose={closeModal} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
export default NFRegex;
|
||||
import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg, BsRegex } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFRegex/ServiceRow';
|
||||
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaFilter } from 'react-icons/fa';
|
||||
import { FaServer } from "react-icons/fa6";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
function NFRegex({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfregexServiceQuery()
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFRegex Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Add a new regex" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfregex" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfregex/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter Regex allows you to filter traffic using regexes</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start a service, add your regexes and it's already done!</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfregex" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{srv?
|
||||
<AddNewRegex opened={open} onClose={closeModal} service={srv} />:
|
||||
<AddEditService opened={open} onClose={closeModal} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
export default NFRegex;
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import ServiceRow from '../../components/PortHijack/ServiceRow';
|
||||
import { porthijackServiceQuery } from '../../components/PortHijack/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddNewService from '../../components/PortHijack/AddNewService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaServer } from 'react-icons/fa';
|
||||
import { GrDirections } from 'react-icons/gr';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
function PortHijack() {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const services = porthijackServiceQuery()
|
||||
|
||||
useEffect(()=>{
|
||||
if(services.isError)
|
||||
errorNotify("Porthijack Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="porthijack" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="porthijack" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
<AddNewService opened={open} onClose={closeModal} />
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
|
||||
export default PortHijack;
|
||||
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import ServiceRow from '../../components/PortHijack/ServiceRow';
|
||||
import { porthijackServiceQuery } from '../../components/PortHijack/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddNewService from '../../components/PortHijack/AddNewService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaServer } from 'react-icons/fa';
|
||||
import { GrDirections } from 'react-icons/gr';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
function PortHijack() {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const services = porthijackServiceQuery()
|
||||
|
||||
useEffect(()=>{
|
||||
if(services.isError)
|
||||
errorNotify("Porthijack Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="porthijack" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="porthijack" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
<AddNewService opened={open} onClose={closeModal} />
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
|
||||
export default PortHijack;
|
||||
|
||||
278
frontend/src/pages/Setup/index.tsx
Normal file
278
frontend/src/pages/Setup/index.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import { Box, Button, Code, Divider, FileButton, Group, List, Paper, Space, Stack, Text, Textarea, ThemeIcon, Title } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaCheck, FaDownload, FaExclamationTriangle, FaTimes, FaUpload } from 'react-icons/fa';
|
||||
import { MdSettings } from 'react-icons/md';
|
||||
import { getapi, isMediumScreen, postapi } from '../../js/utils';
|
||||
import { errorNotify, okNotify } from '../../js/utils';
|
||||
|
||||
export default function SetupPage() {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [importResult, setImportResult] = useState<any>(null);
|
||||
const [configJson, setConfigJson] = useState('');
|
||||
const isMedium = isMediumScreen();
|
||||
|
||||
const handleExport = async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const response = await getapi('/setup/export');
|
||||
const blob = new Blob([JSON.stringify(response, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `firegex-setup-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
okNotify('Configuration exported successfully', '');
|
||||
} catch (err) {
|
||||
errorNotify('Failed to export configuration', String(err));
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportFile = async () => {
|
||||
if (!file) {
|
||||
errorNotify('Please select a file first', '');
|
||||
return;
|
||||
}
|
||||
|
||||
setImporting(true);
|
||||
setImportResult(null);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch('/api/setup/import/file', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Import failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setImportResult(result);
|
||||
|
||||
if (result.status === 'ok') {
|
||||
okNotify('Configuration imported successfully', '');
|
||||
} else {
|
||||
errorNotify('Configuration imported with errors', 'Check the results below');
|
||||
}
|
||||
} catch (err) {
|
||||
errorNotify('Failed to import configuration', String(err));
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportJson = async () => {
|
||||
if (!configJson.trim()) {
|
||||
errorNotify('Please enter a JSON configuration', '');
|
||||
return;
|
||||
}
|
||||
|
||||
setImporting(true);
|
||||
setImportResult(null);
|
||||
|
||||
try {
|
||||
const config = JSON.parse(configJson);
|
||||
const result = await postapi('/setup/import', config);
|
||||
setImportResult(result);
|
||||
|
||||
if (result.status === 'ok') {
|
||||
okNotify('Configuration imported successfully', '');
|
||||
} else {
|
||||
errorNotify('Configuration imported with errors', 'Check the results below');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
errorNotify('Invalid JSON format', String(err));
|
||||
} else {
|
||||
errorNotify('Failed to import configuration', String(err));
|
||||
}
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px="md" mt="lg">
|
||||
<Title order={1} className="center-flex">
|
||||
<ThemeIcon radius="md" size="lg" variant='filled' color='cyan'>
|
||||
<MdSettings size={24} />
|
||||
</ThemeIcon>
|
||||
<Space w="sm" />
|
||||
Setup Import/Export
|
||||
</Title>
|
||||
<Text c="dimmed" mt="sm">
|
||||
Import or export your Firegex configuration including all services and rules
|
||||
</Text>
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
<Stack gap="xl">
|
||||
{/* Export Section */}
|
||||
<Paper shadow="sm" p="lg" withBorder>
|
||||
<Title order={3} mb="md">Export Configuration</Title>
|
||||
<Text c="dimmed" mb="md">
|
||||
Download all current services and rules as a JSON file
|
||||
</Text>
|
||||
<Button
|
||||
leftSection={<FaDownload />}
|
||||
onClick={handleExport}
|
||||
loading={exporting}
|
||||
color="teal"
|
||||
size="md"
|
||||
>
|
||||
Export to JSON
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
{/* Import from File Section */}
|
||||
<Paper shadow="sm" p="lg" withBorder>
|
||||
<Title order={3} mb="md">Import from File</Title>
|
||||
<Text c="dimmed" mb="md">
|
||||
Upload a setup.json file to create services and rules
|
||||
</Text>
|
||||
<Group>
|
||||
<FileButton onChange={setFile} accept="application/json">
|
||||
{(props) => (
|
||||
<Button {...props} variant="outline" color="cyan">
|
||||
{file ? file.name : 'Select JSON File'}
|
||||
</Button>
|
||||
)}
|
||||
</FileButton>
|
||||
<Button
|
||||
leftSection={<FaUpload />}
|
||||
onClick={handleImportFile}
|
||||
loading={importing}
|
||||
disabled={!file}
|
||||
color="blue"
|
||||
size="md"
|
||||
>
|
||||
Import from File
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
{/* Import from JSON Section */}
|
||||
<Paper shadow="sm" p="lg" withBorder>
|
||||
<Title order={3} mb="md">Import from JSON</Title>
|
||||
<Text c="dimmed" mb="md">
|
||||
Paste a JSON configuration directly
|
||||
</Text>
|
||||
<Textarea
|
||||
placeholder='{"services": [], "porthijack": [], "firewall": []}'
|
||||
value={configJson}
|
||||
onChange={(e) => setConfigJson(e.currentTarget.value)}
|
||||
minRows={10}
|
||||
maxRows={20}
|
||||
mb="md"
|
||||
styles={{ input: { fontFamily: 'monospace', fontSize: '12px' } }}
|
||||
/>
|
||||
<Button
|
||||
leftSection={<FaUpload />}
|
||||
onClick={handleImportJson}
|
||||
loading={importing}
|
||||
disabled={!configJson.trim()}
|
||||
color="blue"
|
||||
size="md"
|
||||
>
|
||||
Import from JSON
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
{/* Import Results */}
|
||||
{importResult && (
|
||||
<Paper shadow="sm" p="lg" withBorder>
|
||||
<Title order={3} mb="md">
|
||||
<Group>
|
||||
{importResult.status === 'ok' ? (
|
||||
<ThemeIcon color="teal" size="lg" radius="xl">
|
||||
<FaCheck />
|
||||
</ThemeIcon>
|
||||
) : (
|
||||
<ThemeIcon color="yellow" size="lg" radius="xl">
|
||||
<FaExclamationTriangle />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
Import Results
|
||||
</Group>
|
||||
</Title>
|
||||
|
||||
<Stack gap="md">
|
||||
<Group>
|
||||
<Text fw={500}>Services:</Text>
|
||||
<Text c={importResult.services_created > 0 ? "teal" : "dimmed"}>
|
||||
{importResult.services_created} created
|
||||
</Text>
|
||||
</Group>
|
||||
<Group>
|
||||
<Text fw={500}>PortHijack Services:</Text>
|
||||
<Text c={importResult.porthijack_created > 0 ? "teal" : "dimmed"}>
|
||||
{importResult.porthijack_created} created
|
||||
</Text>
|
||||
</Group>
|
||||
<Group>
|
||||
<Text fw={500}>Firewall Rules:</Text>
|
||||
<Text c={importResult.firewall_created > 0 ? "teal" : "dimmed"}>
|
||||
{importResult.firewall_created} created
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{importResult.errors && importResult.errors.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<Text fw={500} c="red">Errors:</Text>
|
||||
<List
|
||||
spacing="xs"
|
||||
size="sm"
|
||||
icon={
|
||||
<ThemeIcon color="red" size={20} radius="xl">
|
||||
<FaTimes size={12} />
|
||||
</ThemeIcon>
|
||||
}
|
||||
>
|
||||
{importResult.errors.map((error: string, idx: number) => (
|
||||
<List.Item key={idx}>
|
||||
<Code>{error}</Code>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Example Configuration */}
|
||||
<Paper shadow="sm" p="lg" withBorder>
|
||||
<Title order={3} mb="md">Example Configuration</Title>
|
||||
<Text c="dimmed" mb="md">
|
||||
Here's an example of the JSON structure:
|
||||
</Text>
|
||||
<Code block>{`{
|
||||
"services": [
|
||||
{
|
||||
"name": "Example HTTP Service",
|
||||
"port": 8080,
|
||||
"proto": "http",
|
||||
"ip_int": "0.0.0.0",
|
||||
"fail_open": true
|
||||
}
|
||||
],
|
||||
"porthijack": [],
|
||||
"firewall": []
|
||||
}`}</Code>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
307
frontend/src/pages/TrafficViewer/index.tsx
Normal file
307
frontend/src/pages/TrafficViewer/index.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import { ActionIcon, Badge, Box, Card, Divider, Group, LoadingOverlay, Select, Space, Text, TextInput, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
||||
import { isMediumScreen } from '../../js/utils';
|
||||
import { MdDoubleArrow, MdVisibility } from 'react-icons/md';
|
||||
import { TbPlugConnected } from 'react-icons/tb';
|
||||
import { FaFilter, FaServer } from 'react-icons/fa';
|
||||
import { BsRegex } from 'react-icons/bs';
|
||||
import { useState } from 'react';
|
||||
|
||||
type UnifiedService = {
|
||||
service_id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
port: number;
|
||||
proto: string;
|
||||
ip_int: string;
|
||||
type: 'nfproxy' | 'nfregex';
|
||||
stats: {
|
||||
edited_packets?: number;
|
||||
blocked_packets?: number;
|
||||
n_packets?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default function TrafficViewer() {
|
||||
const nfproxyServices = nfproxyServiceQuery();
|
||||
const nfregexServices = nfregexServiceQuery();
|
||||
const navigate = useNavigate();
|
||||
const isMedium = isMediumScreen();
|
||||
const [filterText, setFilterText] = useState('');
|
||||
const [filterType, setFilterType] = useState<string | null>(null);
|
||||
const [filterProto, setFilterProto] = useState<string | null>(null);
|
||||
const [filterStatus, setFilterStatus] = useState<string | null>(null);
|
||||
|
||||
if (nfproxyServices.isLoading || nfregexServices.isLoading) {
|
||||
return <LoadingOverlay visible={true} />;
|
||||
}
|
||||
|
||||
// Combine services from both modules
|
||||
const allServices: UnifiedService[] = [
|
||||
...(nfproxyServices.data?.map(s => ({
|
||||
service_id: s.service_id,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
port: s.port,
|
||||
proto: s.proto,
|
||||
ip_int: s.ip_int,
|
||||
type: 'nfproxy' as const,
|
||||
stats: {
|
||||
edited_packets: s.edited_packets,
|
||||
blocked_packets: s.blocked_packets
|
||||
}
|
||||
})) || []),
|
||||
...(nfregexServices.data?.map(s => ({
|
||||
service_id: s.service_id,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
port: s.port,
|
||||
proto: s.proto,
|
||||
ip_int: s.ip_int,
|
||||
type: 'nfregex' as const,
|
||||
stats: {
|
||||
n_packets: s.n_packets
|
||||
}
|
||||
})) || [])
|
||||
];
|
||||
|
||||
// Apply filters
|
||||
const filteredServices = allServices.filter(service => {
|
||||
// Text filter
|
||||
if (filterText) {
|
||||
const search = filterText.toLowerCase();
|
||||
const matchesText = (
|
||||
service.name.toLowerCase().includes(search) ||
|
||||
service.service_id.toLowerCase().includes(search) ||
|
||||
service.port.toString().includes(search) ||
|
||||
service.ip_int.toLowerCase().includes(search)
|
||||
);
|
||||
if (!matchesText) return false;
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if (filterType && service.type !== filterType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Protocol filter
|
||||
if (filterProto && service.proto !== filterProto) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (filterStatus) {
|
||||
if (filterStatus === 'active' && service.status !== 'active') return false;
|
||||
if (filterStatus === 'stopped' && service.status === 'active') return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const activeServices = filteredServices.filter(s => s.status === 'active');
|
||||
const stoppedServices = filteredServices.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 NFProxy and NFRegex services
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
<Box px="md" mb="lg">
|
||||
<Group grow>
|
||||
<TextInput
|
||||
placeholder="Search by name, ID, port, or IP..."
|
||||
value={filterText}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
|
||||
leftSection={<FaFilter />}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Service Type"
|
||||
clearable
|
||||
value={filterType}
|
||||
onChange={setFilterType}
|
||||
data={[
|
||||
{ value: 'nfproxy', label: 'Netfilter Proxy' },
|
||||
{ value: 'nfregex', label: 'Netfilter Regex' }
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Protocol"
|
||||
clearable
|
||||
value={filterProto}
|
||||
onChange={setFilterProto}
|
||||
data={[
|
||||
{ value: 'tcp', label: 'TCP' },
|
||||
{ value: 'udp', label: 'UDP' },
|
||||
{ value: 'http', label: 'HTTP' }
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Status"
|
||||
clearable
|
||||
value={filterStatus}
|
||||
onChange={setFilterStatus}
|
||||
data={[
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'stopped', label: 'Stopped' }
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
{allServices.length === 0 ? (
|
||||
<Box px="md">
|
||||
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
|
||||
No services found
|
||||
</Title>
|
||||
<Space h="xs" />
|
||||
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
|
||||
Create services in Netfilter Proxy or Netfilter Regex to start monitoring traffic
|
||||
</Text>
|
||||
</Box>
|
||||
) : filteredServices.length === 0 ? (
|
||||
<Box px="md">
|
||||
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
|
||||
No services match your filters
|
||||
</Title>
|
||||
<Space h="xs" />
|
||||
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
|
||||
Try adjusting your filter criteria
|
||||
</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.type}-${service.service_id}`} shadow="sm" padding="lg" radius="md" withBorder mb="md">
|
||||
<Group justify="space-between">
|
||||
<Box>
|
||||
<Group>
|
||||
<ThemeIcon
|
||||
color={service.type === 'nfproxy' ? 'lime' : 'grape'}
|
||||
variant="light"
|
||||
size="lg"
|
||||
>
|
||||
{service.type === 'nfproxy' ? (
|
||||
<TbPlugConnected size={20} />
|
||||
) : (
|
||||
<BsRegex size={20} />
|
||||
)}
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Group gap="xs">
|
||||
<Text fw={700} size="lg">{service.name}</Text>
|
||||
<Badge
|
||||
size="xs"
|
||||
color={service.type === 'nfproxy' ? 'lime' : 'grape'}
|
||||
variant="dot"
|
||||
>
|
||||
{service.type === 'nfproxy' ? 'Proxy' : 'Regex'}
|
||||
</Badge>
|
||||
</Group>
|
||||
<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' }}>
|
||||
{service.type === 'nfproxy' ? (
|
||||
<>
|
||||
<Badge color="orange" size="sm" mb={4}>
|
||||
{service.stats.edited_packets || 0} edited
|
||||
</Badge>
|
||||
<br />
|
||||
<Badge color="yellow" size="sm">
|
||||
{service.stats.blocked_packets || 0} blocked
|
||||
</Badge>
|
||||
</>
|
||||
) : (
|
||||
<Badge color="yellow" size="sm">
|
||||
{service.stats.n_packets || 0} blocked
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
<Tooltip label="View traffic">
|
||||
<ActionIcon
|
||||
color="cyan"
|
||||
size="xl"
|
||||
radius="md"
|
||||
variant="filled"
|
||||
onClick={() => navigate(`/${service.type}/${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={service.type === 'nfproxy' ? 'lime' : 'grape'} variant="light" size="lg">
|
||||
{service.type === 'nfproxy' ? <TbPlugConnected size={18} /> : <BsRegex size={18} />}
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="lg" c="dimmed">{service.name}</Text>
|
||||
<Badge color="gray" size="sm">
|
||||
{service.type === 'nfproxy' ? 'Proxy' : 'Regex'}
|
||||
</Badge>
|
||||
</Group>
|
||||
<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>
|
||||
)}
|
||||
</>;
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
|
||||
2
run.py
2
run.py
@@ -268,7 +268,7 @@ def write_compose(skip_password = True):
|
||||
"firewall": {
|
||||
"restart": "unless-stopped",
|
||||
"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",
|
||||
"environment": [
|
||||
f"PORT={args.port}",
|
||||
|
||||
20
setup.example.json
Normal file
20
setup.example.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"name": "Example HTTP Service",
|
||||
"port": 8080,
|
||||
"proto": "http",
|
||||
"ip_int": "0.0.0.0",
|
||||
"fail_open": true
|
||||
},
|
||||
{
|
||||
"name": "Example TCP Service",
|
||||
"port": 443,
|
||||
"proto": "tcp",
|
||||
"ip_int": "0.0.0.0",
|
||||
"fail_open": false
|
||||
}
|
||||
],
|
||||
"porthijack": [],
|
||||
"firewall": []
|
||||
}
|
||||
@@ -1,51 +1,51 @@
|
||||
0,4090.616
|
||||
1,2211.62
|
||||
2,1165.45
|
||||
3,849.39
|
||||
4,828.635
|
||||
5,741.537
|
||||
6,632.721
|
||||
7,624.772
|
||||
8,529.234
|
||||
9,469.688
|
||||
10,336.33
|
||||
11,427.783
|
||||
12,400.662
|
||||
13,335.086
|
||||
14,342.042
|
||||
15,307.283
|
||||
16,239.694
|
||||
17,295.163
|
||||
18,285.787
|
||||
19,254.402
|
||||
20,250.553
|
||||
21,227.146
|
||||
22,238.747
|
||||
23,234.718
|
||||
24,210.484
|
||||
25,210.697
|
||||
26,205.943
|
||||
27,202.568
|
||||
28,194.341
|
||||
29,189.916
|
||||
30,154.228
|
||||
31,168.922
|
||||
32,173.623
|
||||
33,125.431
|
||||
34,162.154
|
||||
35,149.865
|
||||
36,150.088
|
||||
37,146.085
|
||||
38,137.182
|
||||
39,138.686
|
||||
40,136.302
|
||||
41,132.707
|
||||
42,100.928
|
||||
43,126.414
|
||||
44,125.271
|
||||
45,117.839
|
||||
46,89.494
|
||||
47,116.939
|
||||
48,112.517
|
||||
49,111.369
|
||||
50,108.568
|
||||
0,4090.616
|
||||
1,2211.62
|
||||
2,1165.45
|
||||
3,849.39
|
||||
4,828.635
|
||||
5,741.537
|
||||
6,632.721
|
||||
7,624.772
|
||||
8,529.234
|
||||
9,469.688
|
||||
10,336.33
|
||||
11,427.783
|
||||
12,400.662
|
||||
13,335.086
|
||||
14,342.042
|
||||
15,307.283
|
||||
16,239.694
|
||||
17,295.163
|
||||
18,285.787
|
||||
19,254.402
|
||||
20,250.553
|
||||
21,227.146
|
||||
22,238.747
|
||||
23,234.718
|
||||
24,210.484
|
||||
25,210.697
|
||||
26,205.943
|
||||
27,202.568
|
||||
28,194.341
|
||||
29,189.916
|
||||
30,154.228
|
||||
31,168.922
|
||||
32,173.623
|
||||
33,125.431
|
||||
34,162.154
|
||||
35,149.865
|
||||
36,150.088
|
||||
37,146.085
|
||||
38,137.182
|
||||
39,138.686
|
||||
40,136.302
|
||||
41,132.707
|
||||
42,100.928
|
||||
43,126.414
|
||||
44,125.271
|
||||
45,117.839
|
||||
46,89.494
|
||||
47,116.939
|
||||
48,112.517
|
||||
49,111.369
|
||||
50,108.568
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,3789.988
|
||||
1,2069.487
|
||||
2,1484.554
|
||||
3,956.972
|
||||
4,1052.873
|
||||
5,739.658
|
||||
6,534.722
|
||||
7,638.524
|
||||
8,573.833
|
||||
9,531.658
|
||||
10,476.167
|
||||
11,443.746
|
||||
12,406.027
|
||||
13,385.739
|
||||
14,341.563
|
||||
15,318.699
|
||||
16,303.722
|
||||
17,284.924
|
||||
18,284.336
|
||||
19,267.32
|
||||
20,202.74
|
||||
21,243.849
|
||||
22,226.082
|
||||
23,214.348
|
||||
24,216.8
|
||||
25,188.98
|
||||
26,158.68
|
||||
27,166.556
|
||||
28,148.287
|
||||
29,149.681
|
||||
30,177.043
|
||||
31,175.321
|
||||
32,165.312
|
||||
33,166.943
|
||||
34,159.026
|
||||
35,156.759
|
||||
36,150.216
|
||||
37,144.932
|
||||
38,146.088
|
||||
39,135.897
|
||||
40,136.99
|
||||
41,128.557
|
||||
42,100.307
|
||||
43,103.249
|
||||
44,123.49
|
||||
45,120.39
|
||||
46,118.055
|
||||
47,115.0
|
||||
48,112.593
|
||||
49,109.55
|
||||
50,109.512
|
||||
0,3789.988
|
||||
1,2069.487
|
||||
2,1484.554
|
||||
3,956.972
|
||||
4,1052.873
|
||||
5,739.658
|
||||
6,534.722
|
||||
7,638.524
|
||||
8,573.833
|
||||
9,531.658
|
||||
10,476.167
|
||||
11,443.746
|
||||
12,406.027
|
||||
13,385.739
|
||||
14,341.563
|
||||
15,318.699
|
||||
16,303.722
|
||||
17,284.924
|
||||
18,284.336
|
||||
19,267.32
|
||||
20,202.74
|
||||
21,243.849
|
||||
22,226.082
|
||||
23,214.348
|
||||
24,216.8
|
||||
25,188.98
|
||||
26,158.68
|
||||
27,166.556
|
||||
28,148.287
|
||||
29,149.681
|
||||
30,177.043
|
||||
31,175.321
|
||||
32,165.312
|
||||
33,166.943
|
||||
34,159.026
|
||||
35,156.759
|
||||
36,150.216
|
||||
37,144.932
|
||||
38,146.088
|
||||
39,135.897
|
||||
40,136.99
|
||||
41,128.557
|
||||
42,100.307
|
||||
43,103.249
|
||||
44,123.49
|
||||
45,120.39
|
||||
46,118.055
|
||||
47,115.0
|
||||
48,112.593
|
||||
49,109.55
|
||||
50,109.512
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,4216.05
|
||||
1,4239.598
|
||||
2,2418.527
|
||||
3,2227.8
|
||||
4,2045.351
|
||||
5,2066.161
|
||||
6,2214.416
|
||||
7,2052.845
|
||||
8,2195.199
|
||||
9,2186.867
|
||||
10,2147.534
|
||||
11,2186.652
|
||||
12,2178.036
|
||||
13,2182.151
|
||||
14,2185.324
|
||||
15,1812.911
|
||||
16,2144.689
|
||||
17,2163.525
|
||||
18,2073.89
|
||||
19,2071.682
|
||||
20,2153.502
|
||||
21,2144.04
|
||||
22,2118.517
|
||||
23,2141.19
|
||||
24,2167.103
|
||||
25,2168.631
|
||||
26,2165.555
|
||||
27,2158.424
|
||||
28,2188.376
|
||||
29,2165.311
|
||||
30,2168.158
|
||||
31,2108.045
|
||||
32,2121.414
|
||||
33,2022.533
|
||||
34,1888.759
|
||||
35,2022.837
|
||||
36,2015.042
|
||||
37,1920.401
|
||||
38,2005.037
|
||||
39,2028.856
|
||||
40,2010.43
|
||||
41,1522.342
|
||||
42,1525.635
|
||||
43,1912.05
|
||||
44,1920.256
|
||||
45,1753.645
|
||||
46,1476.977
|
||||
47,1888.645
|
||||
48,1949.103
|
||||
49,1684.633
|
||||
50,1493.935
|
||||
0,4216.05
|
||||
1,4239.598
|
||||
2,2418.527
|
||||
3,2227.8
|
||||
4,2045.351
|
||||
5,2066.161
|
||||
6,2214.416
|
||||
7,2052.845
|
||||
8,2195.199
|
||||
9,2186.867
|
||||
10,2147.534
|
||||
11,2186.652
|
||||
12,2178.036
|
||||
13,2182.151
|
||||
14,2185.324
|
||||
15,1812.911
|
||||
16,2144.689
|
||||
17,2163.525
|
||||
18,2073.89
|
||||
19,2071.682
|
||||
20,2153.502
|
||||
21,2144.04
|
||||
22,2118.517
|
||||
23,2141.19
|
||||
24,2167.103
|
||||
25,2168.631
|
||||
26,2165.555
|
||||
27,2158.424
|
||||
28,2188.376
|
||||
29,2165.311
|
||||
30,2168.158
|
||||
31,2108.045
|
||||
32,2121.414
|
||||
33,2022.533
|
||||
34,1888.759
|
||||
35,2022.837
|
||||
36,2015.042
|
||||
37,1920.401
|
||||
38,2005.037
|
||||
39,2028.856
|
||||
40,2010.43
|
||||
41,1522.342
|
||||
42,1525.635
|
||||
43,1912.05
|
||||
44,1920.256
|
||||
45,1753.645
|
||||
46,1476.977
|
||||
47,1888.645
|
||||
48,1949.103
|
||||
49,1684.633
|
||||
50,1493.935
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,4203.31
|
||||
1,4283.392
|
||||
2,2383.415
|
||||
3,2419.701
|
||||
4,2038.823
|
||||
5,2038.0
|
||||
6,2160.869
|
||||
7,2192.641
|
||||
8,2216.766
|
||||
9,2762.56
|
||||
10,2160.398
|
||||
11,2147.886
|
||||
12,2146.47
|
||||
13,2158.101
|
||||
14,2154.025
|
||||
15,1997.694
|
||||
16,2028.288
|
||||
17,2005.373
|
||||
18,2153.945
|
||||
19,2190.799
|
||||
20,2169.302
|
||||
21,2139.842
|
||||
22,2155.307
|
||||
23,2152.223
|
||||
24,2124.155
|
||||
25,2103.135
|
||||
26,2148.053
|
||||
27,2163.366
|
||||
28,2122.339
|
||||
29,2064.701
|
||||
30,2134.748
|
||||
31,1632.533
|
||||
32,2082.309
|
||||
33,1878.795
|
||||
34,2009.28
|
||||
35,1987.424
|
||||
36,1748.364
|
||||
37,1725.66
|
||||
38,1967.877
|
||||
39,1854.637
|
||||
40,1903.963
|
||||
41,1987.138
|
||||
42,1532.547
|
||||
43,1569.27
|
||||
44,1535.941
|
||||
45,1941.715
|
||||
46,2014.504
|
||||
47,2005.794
|
||||
48,2022.972
|
||||
49,1740.836
|
||||
50,1726.444
|
||||
0,4203.31
|
||||
1,4283.392
|
||||
2,2383.415
|
||||
3,2419.701
|
||||
4,2038.823
|
||||
5,2038.0
|
||||
6,2160.869
|
||||
7,2192.641
|
||||
8,2216.766
|
||||
9,2762.56
|
||||
10,2160.398
|
||||
11,2147.886
|
||||
12,2146.47
|
||||
13,2158.101
|
||||
14,2154.025
|
||||
15,1997.694
|
||||
16,2028.288
|
||||
17,2005.373
|
||||
18,2153.945
|
||||
19,2190.799
|
||||
20,2169.302
|
||||
21,2139.842
|
||||
22,2155.307
|
||||
23,2152.223
|
||||
24,2124.155
|
||||
25,2103.135
|
||||
26,2148.053
|
||||
27,2163.366
|
||||
28,2122.339
|
||||
29,2064.701
|
||||
30,2134.748
|
||||
31,1632.533
|
||||
32,2082.309
|
||||
33,1878.795
|
||||
34,2009.28
|
||||
35,1987.424
|
||||
36,1748.364
|
||||
37,1725.66
|
||||
38,1967.877
|
||||
39,1854.637
|
||||
40,1903.963
|
||||
41,1987.138
|
||||
42,1532.547
|
||||
43,1569.27
|
||||
44,1535.941
|
||||
45,1941.715
|
||||
46,2014.504
|
||||
47,2005.794
|
||||
48,2022.972
|
||||
49,1740.836
|
||||
50,1726.444
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,710.619
|
||||
1,887.877
|
||||
2,981.431
|
||||
3,1081.412
|
||||
4,1038.514
|
||||
5,1029.805
|
||||
6,928.317
|
||||
7,1130.938
|
||||
8,1165.42
|
||||
9,925.632
|
||||
10,949.483
|
||||
11,1021.973
|
||||
12,903.878
|
||||
13,1001.53
|
||||
14,895.351
|
||||
15,1026.722
|
||||
16,634.727
|
||||
17,744.758
|
||||
18,978.59
|
||||
19,962.375
|
||||
20,997.471
|
||||
21,929.785
|
||||
22,1200.83
|
||||
23,1257.741
|
||||
24,772.729
|
||||
25,683.913
|
||||
26,1188.17
|
||||
27,919.961
|
||||
28,922.225
|
||||
29,1066.286
|
||||
30,979.399
|
||||
31,978.917
|
||||
32,988.415
|
||||
33,1061.523
|
||||
34,942.85
|
||||
35,1045.949
|
||||
36,883.941
|
||||
37,958.41
|
||||
38,989.523
|
||||
39,1001.121
|
||||
40,1080.079
|
||||
41,1151.938
|
||||
42,1221.644
|
||||
43,991.855
|
||||
44,1088.344
|
||||
45,973.641
|
||||
46,952.35
|
||||
47,1089.644
|
||||
48,939.615
|
||||
49,1258.419
|
||||
50,949.414
|
||||
0,710.619
|
||||
1,887.877
|
||||
2,981.431
|
||||
3,1081.412
|
||||
4,1038.514
|
||||
5,1029.805
|
||||
6,928.317
|
||||
7,1130.938
|
||||
8,1165.42
|
||||
9,925.632
|
||||
10,949.483
|
||||
11,1021.973
|
||||
12,903.878
|
||||
13,1001.53
|
||||
14,895.351
|
||||
15,1026.722
|
||||
16,634.727
|
||||
17,744.758
|
||||
18,978.59
|
||||
19,962.375
|
||||
20,997.471
|
||||
21,929.785
|
||||
22,1200.83
|
||||
23,1257.741
|
||||
24,772.729
|
||||
25,683.913
|
||||
26,1188.17
|
||||
27,919.961
|
||||
28,922.225
|
||||
29,1066.286
|
||||
30,979.399
|
||||
31,978.917
|
||||
32,988.415
|
||||
33,1061.523
|
||||
34,942.85
|
||||
35,1045.949
|
||||
36,883.941
|
||||
37,958.41
|
||||
38,989.523
|
||||
39,1001.121
|
||||
40,1080.079
|
||||
41,1151.938
|
||||
42,1221.644
|
||||
43,991.855
|
||||
44,1088.344
|
||||
45,973.641
|
||||
46,952.35
|
||||
47,1089.644
|
||||
48,939.615
|
||||
49,1258.419
|
||||
50,949.414
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,3245.763
|
||||
1,3283.646
|
||||
2,3741.157
|
||||
3,3691.206
|
||||
4,3365.134
|
||||
5,3691.457
|
||||
6,3354.807
|
||||
7,3526.728
|
||||
8,3252.62
|
||||
9,3551.086
|
||||
10,3561.506
|
||||
11,3525.577
|
||||
12,2776.064
|
||||
13,3541.86
|
||||
14,3501.34
|
||||
15,3692.092
|
||||
16,3637.166
|
||||
17,3617.031
|
||||
18,3700.092
|
||||
19,3176.831
|
||||
20,3368.038
|
||||
21,3716.577
|
||||
22,3452.917
|
||||
23,3617.604
|
||||
24,3651.796
|
||||
25,3552.053
|
||||
26,3843.18
|
||||
27,3720.406
|
||||
28,3431.1
|
||||
29,3578.973
|
||||
30,3561.994
|
||||
31,3524.566
|
||||
32,3567.537
|
||||
33,3626.767
|
||||
34,3498.361
|
||||
35,3621.396
|
||||
36,3297.839
|
||||
37,3541.207
|
||||
38,3560.364
|
||||
39,3589.746
|
||||
40,3686.673
|
||||
41,3463.811
|
||||
42,3428.408
|
||||
43,3753.139
|
||||
44,3368.89
|
||||
45,3324.876
|
||||
46,3614.895
|
||||
47,3245.942
|
||||
48,3257.925
|
||||
49,3200.585
|
||||
50,3321.55
|
||||
0,3245.763
|
||||
1,3283.646
|
||||
2,3741.157
|
||||
3,3691.206
|
||||
4,3365.134
|
||||
5,3691.457
|
||||
6,3354.807
|
||||
7,3526.728
|
||||
8,3252.62
|
||||
9,3551.086
|
||||
10,3561.506
|
||||
11,3525.577
|
||||
12,2776.064
|
||||
13,3541.86
|
||||
14,3501.34
|
||||
15,3692.092
|
||||
16,3637.166
|
||||
17,3617.031
|
||||
18,3700.092
|
||||
19,3176.831
|
||||
20,3368.038
|
||||
21,3716.577
|
||||
22,3452.917
|
||||
23,3617.604
|
||||
24,3651.796
|
||||
25,3552.053
|
||||
26,3843.18
|
||||
27,3720.406
|
||||
28,3431.1
|
||||
29,3578.973
|
||||
30,3561.994
|
||||
31,3524.566
|
||||
32,3567.537
|
||||
33,3626.767
|
||||
34,3498.361
|
||||
35,3621.396
|
||||
36,3297.839
|
||||
37,3541.207
|
||||
38,3560.364
|
||||
39,3589.746
|
||||
40,3686.673
|
||||
41,3463.811
|
||||
42,3428.408
|
||||
43,3753.139
|
||||
44,3368.89
|
||||
45,3324.876
|
||||
46,3614.895
|
||||
47,3245.942
|
||||
48,3257.925
|
||||
49,3200.585
|
||||
50,3321.55
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,1790.382
|
||||
1,1933.881
|
||||
2,1941.564
|
||||
3,1926.518
|
||||
4,1945.295
|
||||
5,1734.462
|
||||
6,2009.994
|
||||
7,2007.538
|
||||
8,2004.825
|
||||
9,1848.551
|
||||
10,1836.558
|
||||
11,1977.19
|
||||
12,1987.207
|
||||
13,2007.422
|
||||
14,1994.914
|
||||
15,1982.997
|
||||
16,1955.828
|
||||
17,1705.883
|
||||
18,1983.501
|
||||
19,1951.311
|
||||
20,1921.772
|
||||
21,1956.908
|
||||
22,1948.865
|
||||
23,1929.387
|
||||
24,1814.539
|
||||
25,2084.284
|
||||
26,1830.901
|
||||
27,1946.713
|
||||
28,1958.238
|
||||
29,1906.573
|
||||
30,1895.341
|
||||
31,1986.09
|
||||
32,1943.785
|
||||
33,1879.917
|
||||
34,1946.029
|
||||
35,1858.958
|
||||
36,2009.44
|
||||
37,1876.749
|
||||
38,1967.254
|
||||
39,1968.595
|
||||
40,1846.438
|
||||
41,1955.897
|
||||
42,1986.446
|
||||
43,1965.143
|
||||
44,1963.016
|
||||
45,1890.88
|
||||
46,1998.801
|
||||
47,1682.048
|
||||
48,2023.688
|
||||
49,1982.952
|
||||
50,1993.641
|
||||
0,1790.382
|
||||
1,1933.881
|
||||
2,1941.564
|
||||
3,1926.518
|
||||
4,1945.295
|
||||
5,1734.462
|
||||
6,2009.994
|
||||
7,2007.538
|
||||
8,2004.825
|
||||
9,1848.551
|
||||
10,1836.558
|
||||
11,1977.19
|
||||
12,1987.207
|
||||
13,2007.422
|
||||
14,1994.914
|
||||
15,1982.997
|
||||
16,1955.828
|
||||
17,1705.883
|
||||
18,1983.501
|
||||
19,1951.311
|
||||
20,1921.772
|
||||
21,1956.908
|
||||
22,1948.865
|
||||
23,1929.387
|
||||
24,1814.539
|
||||
25,2084.284
|
||||
26,1830.901
|
||||
27,1946.713
|
||||
28,1958.238
|
||||
29,1906.573
|
||||
30,1895.341
|
||||
31,1986.09
|
||||
32,1943.785
|
||||
33,1879.917
|
||||
34,1946.029
|
||||
35,1858.958
|
||||
36,2009.44
|
||||
37,1876.749
|
||||
38,1967.254
|
||||
39,1968.595
|
||||
40,1846.438
|
||||
41,1955.897
|
||||
42,1986.446
|
||||
43,1965.143
|
||||
44,1963.016
|
||||
45,1890.88
|
||||
46,1998.801
|
||||
47,1682.048
|
||||
48,2023.688
|
||||
49,1982.952
|
||||
50,1993.641
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,4007.679
|
||||
1,3963.986
|
||||
2,4222.243
|
||||
3,3640.707
|
||||
4,4388.553
|
||||
5,3636.047
|
||||
6,3644.611
|
||||
7,3547.39
|
||||
8,3412.162
|
||||
9,3632.367
|
||||
10,3536.655
|
||||
11,3820.019
|
||||
12,3677.177
|
||||
13,3366.323
|
||||
14,3353.031
|
||||
15,3392.423
|
||||
16,3330.368
|
||||
17,3363.272
|
||||
18,4027.34
|
||||
19,3467.982
|
||||
20,3607.754
|
||||
21,3767.614
|
||||
22,3340.544
|
||||
23,4086.612
|
||||
24,3784.164
|
||||
25,3496.518
|
||||
26,3543.808
|
||||
27,3453.934
|
||||
28,3546.188
|
||||
29,3458.804
|
||||
30,3728.609
|
||||
31,3697.624
|
||||
32,3698.191
|
||||
33,3673.973
|
||||
34,3690.046
|
||||
35,3663.799
|
||||
36,3540.004
|
||||
37,3857.604
|
||||
38,3426.215
|
||||
39,3704.176
|
||||
40,3796.133
|
||||
41,3604.623
|
||||
42,3650.508
|
||||
43,3501.861
|
||||
44,3685.992
|
||||
45,3623.404
|
||||
46,3728.601
|
||||
47,3844.994
|
||||
48,3820.046
|
||||
49,3680.976
|
||||
50,3797.432
|
||||
0,4007.679
|
||||
1,3963.986
|
||||
2,4222.243
|
||||
3,3640.707
|
||||
4,4388.553
|
||||
5,3636.047
|
||||
6,3644.611
|
||||
7,3547.39
|
||||
8,3412.162
|
||||
9,3632.367
|
||||
10,3536.655
|
||||
11,3820.019
|
||||
12,3677.177
|
||||
13,3366.323
|
||||
14,3353.031
|
||||
15,3392.423
|
||||
16,3330.368
|
||||
17,3363.272
|
||||
18,4027.34
|
||||
19,3467.982
|
||||
20,3607.754
|
||||
21,3767.614
|
||||
22,3340.544
|
||||
23,4086.612
|
||||
24,3784.164
|
||||
25,3496.518
|
||||
26,3543.808
|
||||
27,3453.934
|
||||
28,3546.188
|
||||
29,3458.804
|
||||
30,3728.609
|
||||
31,3697.624
|
||||
32,3698.191
|
||||
33,3673.973
|
||||
34,3690.046
|
||||
35,3663.799
|
||||
36,3540.004
|
||||
37,3857.604
|
||||
38,3426.215
|
||||
39,3704.176
|
||||
40,3796.133
|
||||
41,3604.623
|
||||
42,3650.508
|
||||
43,3501.861
|
||||
44,3685.992
|
||||
45,3623.404
|
||||
46,3728.601
|
||||
47,3844.994
|
||||
48,3820.046
|
||||
49,3680.976
|
||||
50,3797.432
|
||||
|
||||
|
@@ -1,101 +1,101 @@
|
||||
No filters,test data
|
||||
1600.27,1772.897
|
||||
1486.257,1455.93
|
||||
1534.667,1403.539
|
||||
1244.374,1665.846
|
||||
1569.867,1627.449
|
||||
1522.719,1084.153
|
||||
1391.244,1259.783
|
||||
1528.465,1282.901
|
||||
1310.989,1275.515
|
||||
1675.138,1074.39
|
||||
1393.644,1359.139
|
||||
1639.889,1162.937
|
||||
1658.168,1239.767
|
||||
1477.156,1308.195
|
||||
1224.386,1298.007
|
||||
1420.7,1087.031
|
||||
1353.746,1090.502
|
||||
1759.778,1179.381
|
||||
1414.33,1222.86
|
||||
1475.981,1295.207
|
||||
1375.197,1327.8
|
||||
1265.015,1189.121
|
||||
1335.179,1594.98
|
||||
1191.896,1271.873
|
||||
1596.418,1100.372
|
||||
1433.755,1147.945
|
||||
1213.187,1312.989
|
||||
1157.99,1153.825
|
||||
1322.314,1184.481
|
||||
1262.974,1271.012
|
||||
1266.223,1350.519
|
||||
1192.275,1199.142
|
||||
1296.164,1189.432
|
||||
1245.501,1185.107
|
||||
1293.076,1374.689
|
||||
1260.554,1384.055
|
||||
1219.219,1420.395
|
||||
1132.234,1099.141
|
||||
1129.541,1101.805
|
||||
1273.171,1210.564
|
||||
1269.415,1184.094
|
||||
1370.586,1321.974
|
||||
1303.694,1317.074
|
||||
1413.705,1380.092
|
||||
1324.827,1142.097
|
||||
1124.399,1548.557
|
||||
1137.381,1029.353
|
||||
1419.146,1326.829
|
||||
1342.397,1270.316
|
||||
1546.898,1258.933
|
||||
1268.918,1062.23
|
||||
1239.877,1234.887
|
||||
1474.269,1181.184
|
||||
1289.763,1139.728
|
||||
1387.416,1125.734
|
||||
1128.784,1278.381
|
||||
1519.4,1243.597
|
||||
1343.003,1153.18
|
||||
1547.543,1117.816
|
||||
1582.958,1594.145
|
||||
1618.213,1358.087
|
||||
1449.399,1295.487
|
||||
1373.062,1174.153
|
||||
1211.207,1346.833
|
||||
1066.275,1417.633
|
||||
1203.659,1131.727
|
||||
1129.005,1351.061
|
||||
1200.245,1615.952
|
||||
1232.596,1250.436
|
||||
1262.319,1563.46
|
||||
1127.022,1651.89
|
||||
1736.368,1561.661
|
||||
1310.858,1459.713
|
||||
1351.455,1608.494
|
||||
1156.124,1440.379
|
||||
1220.053,1267.708
|
||||
1171.428,1300.284
|
||||
1149.242,1087.453
|
||||
1213.915,1081.207
|
||||
1092.869,1402.761
|
||||
1243.623,1321.907
|
||||
1216.257,1217.721
|
||||
1221.354,1263.695
|
||||
1242.771,1241.684
|
||||
1427.276,1322.01
|
||||
1328.502,1346.21
|
||||
1275.719,1269.909
|
||||
1372.075,1451.069
|
||||
1486.541,1532.56
|
||||
1577.036,1539.804
|
||||
1628.025,1372.806
|
||||
1415.623,1239.201
|
||||
1198.632,1095.849
|
||||
1170.341,1255.875
|
||||
1214.99,1424.292
|
||||
1356.431,1135.588
|
||||
1817.822,1212.386
|
||||
1745.199,1170.863
|
||||
1779.083,1145.458
|
||||
1544.934,1076.386
|
||||
No filters,test data
|
||||
1600.27,1772.897
|
||||
1486.257,1455.93
|
||||
1534.667,1403.539
|
||||
1244.374,1665.846
|
||||
1569.867,1627.449
|
||||
1522.719,1084.153
|
||||
1391.244,1259.783
|
||||
1528.465,1282.901
|
||||
1310.989,1275.515
|
||||
1675.138,1074.39
|
||||
1393.644,1359.139
|
||||
1639.889,1162.937
|
||||
1658.168,1239.767
|
||||
1477.156,1308.195
|
||||
1224.386,1298.007
|
||||
1420.7,1087.031
|
||||
1353.746,1090.502
|
||||
1759.778,1179.381
|
||||
1414.33,1222.86
|
||||
1475.981,1295.207
|
||||
1375.197,1327.8
|
||||
1265.015,1189.121
|
||||
1335.179,1594.98
|
||||
1191.896,1271.873
|
||||
1596.418,1100.372
|
||||
1433.755,1147.945
|
||||
1213.187,1312.989
|
||||
1157.99,1153.825
|
||||
1322.314,1184.481
|
||||
1262.974,1271.012
|
||||
1266.223,1350.519
|
||||
1192.275,1199.142
|
||||
1296.164,1189.432
|
||||
1245.501,1185.107
|
||||
1293.076,1374.689
|
||||
1260.554,1384.055
|
||||
1219.219,1420.395
|
||||
1132.234,1099.141
|
||||
1129.541,1101.805
|
||||
1273.171,1210.564
|
||||
1269.415,1184.094
|
||||
1370.586,1321.974
|
||||
1303.694,1317.074
|
||||
1413.705,1380.092
|
||||
1324.827,1142.097
|
||||
1124.399,1548.557
|
||||
1137.381,1029.353
|
||||
1419.146,1326.829
|
||||
1342.397,1270.316
|
||||
1546.898,1258.933
|
||||
1268.918,1062.23
|
||||
1239.877,1234.887
|
||||
1474.269,1181.184
|
||||
1289.763,1139.728
|
||||
1387.416,1125.734
|
||||
1128.784,1278.381
|
||||
1519.4,1243.597
|
||||
1343.003,1153.18
|
||||
1547.543,1117.816
|
||||
1582.958,1594.145
|
||||
1618.213,1358.087
|
||||
1449.399,1295.487
|
||||
1373.062,1174.153
|
||||
1211.207,1346.833
|
||||
1066.275,1417.633
|
||||
1203.659,1131.727
|
||||
1129.005,1351.061
|
||||
1200.245,1615.952
|
||||
1232.596,1250.436
|
||||
1262.319,1563.46
|
||||
1127.022,1651.89
|
||||
1736.368,1561.661
|
||||
1310.858,1459.713
|
||||
1351.455,1608.494
|
||||
1156.124,1440.379
|
||||
1220.053,1267.708
|
||||
1171.428,1300.284
|
||||
1149.242,1087.453
|
||||
1213.915,1081.207
|
||||
1092.869,1402.761
|
||||
1243.623,1321.907
|
||||
1216.257,1217.721
|
||||
1221.354,1263.695
|
||||
1242.771,1241.684
|
||||
1427.276,1322.01
|
||||
1328.502,1346.21
|
||||
1275.719,1269.909
|
||||
1372.075,1451.069
|
||||
1486.541,1532.56
|
||||
1577.036,1539.804
|
||||
1628.025,1372.806
|
||||
1415.623,1239.201
|
||||
1198.632,1095.849
|
||||
1170.341,1255.875
|
||||
1214.99,1424.292
|
||||
1356.431,1135.588
|
||||
1817.822,1212.386
|
||||
1745.199,1170.863
|
||||
1779.083,1145.458
|
||||
1544.934,1076.386
|
||||
|
||||
|
@@ -1,101 +1,101 @@
|
||||
No filters,test data
|
||||
2098.666,2118.781
|
||||
2175.2,2086.957
|
||||
2177.653,1795.287
|
||||
1775.63,1745.066
|
||||
1827.78,2038.921
|
||||
1813.369,2179.81
|
||||
1988.859,2176.883
|
||||
1634.541,1704.071
|
||||
1878.829,1869.999
|
||||
1738.987,2024.959
|
||||
1920.502,1477.726
|
||||
1895.909,1732.832
|
||||
1812.012,1850.978
|
||||
1908.106,1902.953
|
||||
2112.837,1726.547
|
||||
1765.808,1710.915
|
||||
1918.121,1900.619
|
||||
1892.779,2054.93
|
||||
1852.952,2113.928
|
||||
1713.67,1770.379
|
||||
1873.637,2011.518
|
||||
1787.007,2104.061
|
||||
1764.704,2134.151
|
||||
2064.776,2073.226
|
||||
1838.23,1762.436
|
||||
1808.339,1792.41
|
||||
1756.516,1706.501
|
||||
1665.888,1610.771
|
||||
1682.272,1650.033
|
||||
1690.473,1563.995
|
||||
1997.801,1955.53
|
||||
1660.487,1669.25
|
||||
2023.106,1727.046
|
||||
1724.59,1686.137
|
||||
1697.656,1627.136
|
||||
1689.65,1571.13
|
||||
1628.35,1699.239
|
||||
1843.768,1825.739
|
||||
1715.158,1573.695
|
||||
1732.695,1875.656
|
||||
1902.818,1968.505
|
||||
1699.277,1919.737
|
||||
1618.75,2015.258
|
||||
1696.055,2014.261
|
||||
1792.486,1606.754
|
||||
1889.583,1625.965
|
||||
1716.951,1572.049
|
||||
1727.305,1649.502
|
||||
1747.618,2099.787
|
||||
1698.546,2153.363
|
||||
1723.117,1637.074
|
||||
1654.061,1721.968
|
||||
1735.332,1587.906
|
||||
1841.808,1565.797
|
||||
2006.973,1665.615
|
||||
1730.909,1883.505
|
||||
1681.954,1553.826
|
||||
1653.215,1849.824
|
||||
2072.138,1990.474
|
||||
1792.302,2176.718
|
||||
1679.381,2128.083
|
||||
1653.368,2078.013
|
||||
1399.58,2065.031
|
||||
1669.979,1815.553
|
||||
1677.346,1870.055
|
||||
1652.22,2010.441
|
||||
1870.35,1687.893
|
||||
1772.229,1857.193
|
||||
1743.552,1813.027
|
||||
1685.312,1466.505
|
||||
1863.269,1813.398
|
||||
1694.335,1889.661
|
||||
1739.016,1740.381
|
||||
1764.462,1752.725
|
||||
1702.134,2069.289
|
||||
1955.771,2176.617
|
||||
2046.117,2137.499
|
||||
1766.64,2177.955
|
||||
1733.26,2148.497
|
||||
1834.827,2161.573
|
||||
2087.089,2119.311
|
||||
2154.753,1679.596
|
||||
2073.729,1912.012
|
||||
2082.37,1841.045
|
||||
2160.86,1813.257
|
||||
1678.515,1894.864
|
||||
1758.394,1884.985
|
||||
1673.919,1732.373
|
||||
1666.474,1737.66
|
||||
1679.444,1463.082
|
||||
1684.006,2002.343
|
||||
1737.287,2026.394
|
||||
1811.305,2084.689
|
||||
2127.121,2117.391
|
||||
2139.884,1984.606
|
||||
1677.256,1770.76
|
||||
1698.544,1833.011
|
||||
1905.446,1734.777
|
||||
1913.257,1688.401
|
||||
2063.73,1667.27
|
||||
No filters,test data
|
||||
2098.666,2118.781
|
||||
2175.2,2086.957
|
||||
2177.653,1795.287
|
||||
1775.63,1745.066
|
||||
1827.78,2038.921
|
||||
1813.369,2179.81
|
||||
1988.859,2176.883
|
||||
1634.541,1704.071
|
||||
1878.829,1869.999
|
||||
1738.987,2024.959
|
||||
1920.502,1477.726
|
||||
1895.909,1732.832
|
||||
1812.012,1850.978
|
||||
1908.106,1902.953
|
||||
2112.837,1726.547
|
||||
1765.808,1710.915
|
||||
1918.121,1900.619
|
||||
1892.779,2054.93
|
||||
1852.952,2113.928
|
||||
1713.67,1770.379
|
||||
1873.637,2011.518
|
||||
1787.007,2104.061
|
||||
1764.704,2134.151
|
||||
2064.776,2073.226
|
||||
1838.23,1762.436
|
||||
1808.339,1792.41
|
||||
1756.516,1706.501
|
||||
1665.888,1610.771
|
||||
1682.272,1650.033
|
||||
1690.473,1563.995
|
||||
1997.801,1955.53
|
||||
1660.487,1669.25
|
||||
2023.106,1727.046
|
||||
1724.59,1686.137
|
||||
1697.656,1627.136
|
||||
1689.65,1571.13
|
||||
1628.35,1699.239
|
||||
1843.768,1825.739
|
||||
1715.158,1573.695
|
||||
1732.695,1875.656
|
||||
1902.818,1968.505
|
||||
1699.277,1919.737
|
||||
1618.75,2015.258
|
||||
1696.055,2014.261
|
||||
1792.486,1606.754
|
||||
1889.583,1625.965
|
||||
1716.951,1572.049
|
||||
1727.305,1649.502
|
||||
1747.618,2099.787
|
||||
1698.546,2153.363
|
||||
1723.117,1637.074
|
||||
1654.061,1721.968
|
||||
1735.332,1587.906
|
||||
1841.808,1565.797
|
||||
2006.973,1665.615
|
||||
1730.909,1883.505
|
||||
1681.954,1553.826
|
||||
1653.215,1849.824
|
||||
2072.138,1990.474
|
||||
1792.302,2176.718
|
||||
1679.381,2128.083
|
||||
1653.368,2078.013
|
||||
1399.58,2065.031
|
||||
1669.979,1815.553
|
||||
1677.346,1870.055
|
||||
1652.22,2010.441
|
||||
1870.35,1687.893
|
||||
1772.229,1857.193
|
||||
1743.552,1813.027
|
||||
1685.312,1466.505
|
||||
1863.269,1813.398
|
||||
1694.335,1889.661
|
||||
1739.016,1740.381
|
||||
1764.462,1752.725
|
||||
1702.134,2069.289
|
||||
1955.771,2176.617
|
||||
2046.117,2137.499
|
||||
1766.64,2177.955
|
||||
1733.26,2148.497
|
||||
1834.827,2161.573
|
||||
2087.089,2119.311
|
||||
2154.753,1679.596
|
||||
2073.729,1912.012
|
||||
2082.37,1841.045
|
||||
2160.86,1813.257
|
||||
1678.515,1894.864
|
||||
1758.394,1884.985
|
||||
1673.919,1732.373
|
||||
1666.474,1737.66
|
||||
1679.444,1463.082
|
||||
1684.006,2002.343
|
||||
1737.287,2026.394
|
||||
1811.305,2084.689
|
||||
2127.121,2117.391
|
||||
2139.884,1984.606
|
||||
1677.256,1770.76
|
||||
1698.544,1833.011
|
||||
1905.446,1734.777
|
||||
1913.257,1688.401
|
||||
2063.73,1667.27
|
||||
|
||||
|
@@ -1,101 +1,101 @@
|
||||
No filters,test data
|
||||
3841.832,3177.356
|
||||
3369.899,3819.926
|
||||
3884.689,2843.759
|
||||
3391.267,3106.399
|
||||
3740.054,2899.246
|
||||
3754.086,3254.525
|
||||
3284.178,3180.96
|
||||
3293.044,3356.928
|
||||
3653.05,2925.883
|
||||
3830.609,2784.715
|
||||
3691.078,3283.715
|
||||
3551.286,3437.899
|
||||
3651.296,2759.088
|
||||
3726.295,3289.184
|
||||
3860.353,3067.069
|
||||
3910.997,3764.354
|
||||
3775.794,3182.171
|
||||
3824.719,3376.774
|
||||
3245.109,2954.582
|
||||
3705.489,4101.548
|
||||
3484.114,3155.55
|
||||
3742.727,3153.767
|
||||
3964.472,3624.241
|
||||
3747.219,2787.965
|
||||
3746.575,3518.095
|
||||
3903.7,2942.676
|
||||
3888.772,3222.041
|
||||
3854.913,2479.502
|
||||
3716.801,2876.082
|
||||
3919.146,2748.543
|
||||
3908.195,2742.45
|
||||
3894.436,3135.703
|
||||
3615.381,3411.222
|
||||
3807.51,3525.049
|
||||
3197.936,3515.207
|
||||
3817.654,3505.676
|
||||
3604.482,3749.862
|
||||
4054.217,3389.18
|
||||
4064.973,3110.13
|
||||
3828.174,3994.395
|
||||
3464.949,3706.928
|
||||
3458.833,3818.998
|
||||
3447.594,3354.733
|
||||
3148.49,2938.606
|
||||
3403.617,3000.615
|
||||
3619.143,3712.188
|
||||
3676.835,3294.72
|
||||
4020.2,3668.025
|
||||
3365.03,3288.992
|
||||
3395.001,3047.487
|
||||
3444.301,3644.15
|
||||
3258.341,3412.968
|
||||
3640.787,3028.915
|
||||
3523.975,2984.702
|
||||
3661.891,3124.492
|
||||
3802.303,3098.351
|
||||
3774.646,3486.505
|
||||
3622.705,1967.98
|
||||
3508.677,2629.166
|
||||
3566.014,2717.307
|
||||
3849.619,1697.053
|
||||
3315.839,1708.413
|
||||
3423.282,2104.829
|
||||
3750.536,2822.277
|
||||
3554.167,2610.241
|
||||
3826.747,3645.146
|
||||
3892.643,2795.429
|
||||
3832.114,2572.367
|
||||
3497.325,3586.324
|
||||
3348.139,3108.224
|
||||
3317.933,2944.826
|
||||
3605.83,2890.459
|
||||
3539.072,3132.536
|
||||
3121.903,3343.355
|
||||
2942.032,3478.153
|
||||
3445.076,3762.927
|
||||
3100.771,3377.621
|
||||
3189.105,3326.58
|
||||
3281.825,3443.852
|
||||
2678.243,3830.363
|
||||
2955.651,2863.628
|
||||
2696.034,3640.54
|
||||
3370.494,3203.94
|
||||
3300.628,3755.641
|
||||
3488.021,3931.192
|
||||
3330.963,2780.609
|
||||
3154.885,2986.501
|
||||
3375.716,3359.562
|
||||
2841.549,3077.406
|
||||
3404.81,3385.657
|
||||
3757.787,3352.594
|
||||
3717.258,3264.236
|
||||
3353.01,3659.337
|
||||
3190.808,3732.121
|
||||
3165.985,3380.969
|
||||
3797.661,3264.325
|
||||
3347.68,3711.328
|
||||
3604.306,3454.656
|
||||
3615.091,3547.976
|
||||
3291.287,3115.255
|
||||
No filters,test data
|
||||
3841.832,3177.356
|
||||
3369.899,3819.926
|
||||
3884.689,2843.759
|
||||
3391.267,3106.399
|
||||
3740.054,2899.246
|
||||
3754.086,3254.525
|
||||
3284.178,3180.96
|
||||
3293.044,3356.928
|
||||
3653.05,2925.883
|
||||
3830.609,2784.715
|
||||
3691.078,3283.715
|
||||
3551.286,3437.899
|
||||
3651.296,2759.088
|
||||
3726.295,3289.184
|
||||
3860.353,3067.069
|
||||
3910.997,3764.354
|
||||
3775.794,3182.171
|
||||
3824.719,3376.774
|
||||
3245.109,2954.582
|
||||
3705.489,4101.548
|
||||
3484.114,3155.55
|
||||
3742.727,3153.767
|
||||
3964.472,3624.241
|
||||
3747.219,2787.965
|
||||
3746.575,3518.095
|
||||
3903.7,2942.676
|
||||
3888.772,3222.041
|
||||
3854.913,2479.502
|
||||
3716.801,2876.082
|
||||
3919.146,2748.543
|
||||
3908.195,2742.45
|
||||
3894.436,3135.703
|
||||
3615.381,3411.222
|
||||
3807.51,3525.049
|
||||
3197.936,3515.207
|
||||
3817.654,3505.676
|
||||
3604.482,3749.862
|
||||
4054.217,3389.18
|
||||
4064.973,3110.13
|
||||
3828.174,3994.395
|
||||
3464.949,3706.928
|
||||
3458.833,3818.998
|
||||
3447.594,3354.733
|
||||
3148.49,2938.606
|
||||
3403.617,3000.615
|
||||
3619.143,3712.188
|
||||
3676.835,3294.72
|
||||
4020.2,3668.025
|
||||
3365.03,3288.992
|
||||
3395.001,3047.487
|
||||
3444.301,3644.15
|
||||
3258.341,3412.968
|
||||
3640.787,3028.915
|
||||
3523.975,2984.702
|
||||
3661.891,3124.492
|
||||
3802.303,3098.351
|
||||
3774.646,3486.505
|
||||
3622.705,1967.98
|
||||
3508.677,2629.166
|
||||
3566.014,2717.307
|
||||
3849.619,1697.053
|
||||
3315.839,1708.413
|
||||
3423.282,2104.829
|
||||
3750.536,2822.277
|
||||
3554.167,2610.241
|
||||
3826.747,3645.146
|
||||
3892.643,2795.429
|
||||
3832.114,2572.367
|
||||
3497.325,3586.324
|
||||
3348.139,3108.224
|
||||
3317.933,2944.826
|
||||
3605.83,2890.459
|
||||
3539.072,3132.536
|
||||
3121.903,3343.355
|
||||
2942.032,3478.153
|
||||
3445.076,3762.927
|
||||
3100.771,3377.621
|
||||
3189.105,3326.58
|
||||
3281.825,3443.852
|
||||
2678.243,3830.363
|
||||
2955.651,2863.628
|
||||
2696.034,3640.54
|
||||
3370.494,3203.94
|
||||
3300.628,3755.641
|
||||
3488.021,3931.192
|
||||
3330.963,2780.609
|
||||
3154.885,2986.501
|
||||
3375.716,3359.562
|
||||
2841.549,3077.406
|
||||
3404.81,3385.657
|
||||
3757.787,3352.594
|
||||
3717.258,3264.236
|
||||
3353.01,3659.337
|
||||
3190.808,3732.121
|
||||
3165.985,3380.969
|
||||
3797.661,3264.325
|
||||
3347.68,3711.328
|
||||
3604.306,3454.656
|
||||
3615.091,3547.976
|
||||
3291.287,3115.255
|
||||
|
||||
|
@@ -1,101 +1,101 @@
|
||||
No filters,test data
|
||||
4244.309,3423.133
|
||||
4172.153,3839.874
|
||||
4318.167,3651.161
|
||||
4141.307,3886.542
|
||||
4153.546,3293.166
|
||||
4313.574,3639.47
|
||||
4212.2,3422.614
|
||||
3944.194,3928.898
|
||||
3470.867,3395.562
|
||||
3680.557,4233.545
|
||||
3639.904,3739.869
|
||||
3601.206,4331.278
|
||||
3602.268,3561.573
|
||||
4041.709,3360.442
|
||||
3326.243,3898.576
|
||||
3519.295,3710.73
|
||||
3421.704,3785.601
|
||||
3761.544,3720.579
|
||||
3849.834,3419.051
|
||||
3771.48,3525.297
|
||||
3477.096,3709.462
|
||||
3752.154,3410.653
|
||||
3828.539,3784.068
|
||||
3601.283,4371.022
|
||||
3550.535,3353.485
|
||||
3573.931,4326.953
|
||||
3989.022,3630.239
|
||||
3758.771,3187.932
|
||||
3764.081,3348.153
|
||||
3552.11,3210.788
|
||||
3624.703,3580.683
|
||||
3495.138,3702.232
|
||||
3679.786,3211.763
|
||||
3965.941,4386.728
|
||||
3481.692,4312.93
|
||||
3472.266,3638.52
|
||||
3902.087,4356.89
|
||||
4162.868,3770.82
|
||||
3556.674,3899.06
|
||||
3568.287,3768.694
|
||||
3813.52,3794.494
|
||||
3538.6,4233.813
|
||||
3583.165,3598.301
|
||||
3545.668,3574.602
|
||||
3498.538,3731.551
|
||||
4069.232,3732.176
|
||||
3488.875,4390.112
|
||||
3471.224,4308.19
|
||||
3487.893,3713.36
|
||||
3556.706,3783.748
|
||||
4134.049,4075.267
|
||||
3619.571,3616.779
|
||||
3880.411,4017.523
|
||||
3437.287,4024.127
|
||||
3571.923,4136.496
|
||||
3355.569,4297.359
|
||||
3621.019,3428.405
|
||||
3432.623,3962.733
|
||||
3541.66,3558.748
|
||||
3506.787,3874.117
|
||||
4124.636,3616.127
|
||||
3585.123,3360.593
|
||||
3572.09,3416.381
|
||||
3344.338,3861.743
|
||||
3540.41,3412.915
|
||||
3768.322,3490.888
|
||||
3865.742,3149.312
|
||||
3543.772,3438.211
|
||||
3649.759,3538.124
|
||||
3714.508,3298.845
|
||||
3989.119,3652.572
|
||||
4004.341,3688.486
|
||||
3942.733,3533.375
|
||||
3767.707,3692.636
|
||||
3854.87,3567.363
|
||||
3818.102,4325.471
|
||||
4326.545,3464.113
|
||||
3331.279,3346.4
|
||||
3782.928,3599.129
|
||||
3441.486,3571.214
|
||||
3688.115,3778.354
|
||||
3523.493,4268.157
|
||||
3350.288,3241.872
|
||||
3337.668,3405.69
|
||||
3467.795,3655.209
|
||||
3695.322,3161.427
|
||||
4111.114,3289.313
|
||||
3499.726,3157.723
|
||||
3731.525,3334.048
|
||||
4226.314,3315.567
|
||||
3430.903,3176.271
|
||||
3480.629,3296.73
|
||||
3930.84,3302.929
|
||||
3702.883,3251.164
|
||||
3839.087,3180.461
|
||||
3831.296,3215.8
|
||||
3615.657,3262.533
|
||||
3766.269,3446.736
|
||||
3556.331,4274.897
|
||||
3843.934,3370.384
|
||||
No filters,test data
|
||||
4244.309,3423.133
|
||||
4172.153,3839.874
|
||||
4318.167,3651.161
|
||||
4141.307,3886.542
|
||||
4153.546,3293.166
|
||||
4313.574,3639.47
|
||||
4212.2,3422.614
|
||||
3944.194,3928.898
|
||||
3470.867,3395.562
|
||||
3680.557,4233.545
|
||||
3639.904,3739.869
|
||||
3601.206,4331.278
|
||||
3602.268,3561.573
|
||||
4041.709,3360.442
|
||||
3326.243,3898.576
|
||||
3519.295,3710.73
|
||||
3421.704,3785.601
|
||||
3761.544,3720.579
|
||||
3849.834,3419.051
|
||||
3771.48,3525.297
|
||||
3477.096,3709.462
|
||||
3752.154,3410.653
|
||||
3828.539,3784.068
|
||||
3601.283,4371.022
|
||||
3550.535,3353.485
|
||||
3573.931,4326.953
|
||||
3989.022,3630.239
|
||||
3758.771,3187.932
|
||||
3764.081,3348.153
|
||||
3552.11,3210.788
|
||||
3624.703,3580.683
|
||||
3495.138,3702.232
|
||||
3679.786,3211.763
|
||||
3965.941,4386.728
|
||||
3481.692,4312.93
|
||||
3472.266,3638.52
|
||||
3902.087,4356.89
|
||||
4162.868,3770.82
|
||||
3556.674,3899.06
|
||||
3568.287,3768.694
|
||||
3813.52,3794.494
|
||||
3538.6,4233.813
|
||||
3583.165,3598.301
|
||||
3545.668,3574.602
|
||||
3498.538,3731.551
|
||||
4069.232,3732.176
|
||||
3488.875,4390.112
|
||||
3471.224,4308.19
|
||||
3487.893,3713.36
|
||||
3556.706,3783.748
|
||||
4134.049,4075.267
|
||||
3619.571,3616.779
|
||||
3880.411,4017.523
|
||||
3437.287,4024.127
|
||||
3571.923,4136.496
|
||||
3355.569,4297.359
|
||||
3621.019,3428.405
|
||||
3432.623,3962.733
|
||||
3541.66,3558.748
|
||||
3506.787,3874.117
|
||||
4124.636,3616.127
|
||||
3585.123,3360.593
|
||||
3572.09,3416.381
|
||||
3344.338,3861.743
|
||||
3540.41,3412.915
|
||||
3768.322,3490.888
|
||||
3865.742,3149.312
|
||||
3543.772,3438.211
|
||||
3649.759,3538.124
|
||||
3714.508,3298.845
|
||||
3989.119,3652.572
|
||||
4004.341,3688.486
|
||||
3942.733,3533.375
|
||||
3767.707,3692.636
|
||||
3854.87,3567.363
|
||||
3818.102,4325.471
|
||||
4326.545,3464.113
|
||||
3331.279,3346.4
|
||||
3782.928,3599.129
|
||||
3441.486,3571.214
|
||||
3688.115,3778.354
|
||||
3523.493,4268.157
|
||||
3350.288,3241.872
|
||||
3337.668,3405.69
|
||||
3467.795,3655.209
|
||||
3695.322,3161.427
|
||||
4111.114,3289.313
|
||||
3499.726,3157.723
|
||||
3731.525,3334.048
|
||||
4226.314,3315.567
|
||||
3430.903,3176.271
|
||||
3480.629,3296.73
|
||||
3930.84,3302.929
|
||||
3702.883,3251.164
|
||||
3839.087,3180.461
|
||||
3831.296,3215.8
|
||||
3615.657,3262.533
|
||||
3766.269,3446.736
|
||||
3556.331,4274.897
|
||||
3843.934,3370.384
|
||||
|
||||
|
Reference in New Issue
Block a user