Compare commits
17 Commits
88f4f54b55
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c387d9b40b | ||
|
|
d8061985d6 | ||
|
|
c237112077 | ||
|
|
811773e009 | ||
|
|
f1ebada95d | ||
|
|
9af3023a37 | ||
|
|
16f96aa6f6 | ||
|
|
49c6c14fe5 | ||
|
|
b352790f64 | ||
|
|
f3024cc9a8 | ||
|
|
753ed241b6 | ||
|
|
676e1dcb77 | ||
|
|
0c5a681f5b | ||
|
|
c726855b1c | ||
|
|
bb4addf590 | ||
|
|
f554ac558a | ||
|
|
0492f16cea |
420
.github/workflows/docker-image.yml
vendored
420
.github/workflows/docker-image.yml
vendored
@@ -1,210 +1,210 @@
|
|||||||
name: Create and publish Docker images
|
name: Create and publish Docker images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
- published
|
- published
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker_build:
|
docker_build:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
arch: amd64
|
arch: amd64
|
||||||
run_tests: true
|
run_tests: true
|
||||||
- os: ubuntu-24.04-arm
|
- os: ubuntu-24.04-arm
|
||||||
arch: arm64
|
arch: arm64
|
||||||
run_tests: true
|
run_tests: true
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Convert repository name to lowercase
|
- name: Convert repository name to lowercase
|
||||||
id: lowercase
|
id: lowercase
|
||||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build and run firegex
|
- name: Build and run firegex
|
||||||
if: matrix.run_tests
|
if: matrix.run_tests
|
||||||
run: python3 run.py start -P testpassword
|
run: python3 run.py start -P testpassword
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
if: matrix.run_tests
|
if: matrix.run_tests
|
||||||
run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh
|
run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@master
|
uses: docker/setup-buildx-action@master
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}
|
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Extract tag name
|
||||||
id: tag
|
id: tag
|
||||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Update version in setup.py
|
- name: Update version in setup.py
|
||||||
run: >-
|
run: >-
|
||||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" backend/utils/__init__.py;
|
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" backend/utils/__init__.py;
|
||||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
platforms: linux/${{ matrix.arch }}
|
platforms: linux/${{ matrix.arch }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }}
|
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }}
|
||||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }}
|
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||||
provenance: false
|
provenance: false
|
||||||
sbom: false
|
sbom: false
|
||||||
|
|
||||||
docker_manifest:
|
docker_manifest:
|
||||||
needs: docker_build
|
needs: docker_build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Convert repository name to lowercase
|
- name: Convert repository name to lowercase
|
||||||
id: lowercase
|
id: lowercase
|
||||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Extract tag name
|
||||||
id: tag
|
id: tag
|
||||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Create and push multi-platform manifest
|
- name: Create and push multi-platform manifest
|
||||||
run: |
|
run: |
|
||||||
# Create manifest list for specific tag
|
# Create manifest list for specific tag
|
||||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
||||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64
|
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64
|
||||||
|
|
||||||
# Annotate the manifest with architecture info
|
# Annotate the manifest with architecture info
|
||||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
||||||
--arch amd64 --os linux
|
--arch amd64 --os linux
|
||||||
|
|
||||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 \
|
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 \
|
||||||
--arch arm64 --os linux
|
--arch arm64 --os linux
|
||||||
|
|
||||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}
|
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}
|
||||||
|
|
||||||
# Create manifest list for latest tag
|
# Create manifest list for latest tag
|
||||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64
|
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64
|
||||||
|
|
||||||
# Annotate the latest manifest with architecture info
|
# Annotate the latest manifest with architecture info
|
||||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||||
--arch amd64 --os linux
|
--arch amd64 --os linux
|
||||||
|
|
||||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \
|
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \
|
||||||
--arch arm64 --os linux
|
--arch arm64 --os linux
|
||||||
|
|
||||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest
|
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest
|
||||||
|
|
||||||
create-rootfs-assets:
|
create-rootfs-assets:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [docker_manifest]
|
needs: [docker_manifest]
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: read
|
packages: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Convert repository name to lowercase
|
- name: Convert repository name to lowercase
|
||||||
id: lowercase
|
id: lowercase
|
||||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@master
|
uses: docker/setup-qemu-action@master
|
||||||
with:
|
with:
|
||||||
platforms: all
|
platforms: all
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@master
|
uses: docker/setup-buildx-action@master
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Get latest release tag
|
- name: Get latest release tag
|
||||||
id: get_tag
|
id: get_tag
|
||||||
run: |
|
run: |
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')
|
LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')
|
||||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
echo "Latest release tag: $LATEST_TAG"
|
echo "Latest release tag: $LATEST_TAG"
|
||||||
|
|
||||||
- name: Export rootfs for amd64
|
- name: Export rootfs for amd64
|
||||||
run: |
|
run: |
|
||||||
echo "Creating and exporting amd64 container..."
|
echo "Creating and exporting amd64 container..."
|
||||||
CONTAINER_ID=$(docker create --platform linux/amd64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
CONTAINER_ID=$(docker create --platform linux/amd64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
||||||
docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar"
|
docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar"
|
||||||
docker rm $CONTAINER_ID
|
docker rm $CONTAINER_ID
|
||||||
echo "Compressing amd64 rootfs..."
|
echo "Compressing amd64 rootfs..."
|
||||||
gzip firegex-rootfs-amd64.tar
|
gzip firegex-rootfs-amd64.tar
|
||||||
ls -lh firegex-rootfs-amd64.tar.gz
|
ls -lh firegex-rootfs-amd64.tar.gz
|
||||||
|
|
||||||
- name: Export rootfs for arm64
|
- name: Export rootfs for arm64
|
||||||
run: |
|
run: |
|
||||||
echo "Creating and exporting arm64 container..."
|
echo "Creating and exporting arm64 container..."
|
||||||
CONTAINER_ID=$(docker create --platform linux/arm64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
CONTAINER_ID=$(docker create --platform linux/arm64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
||||||
docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar"
|
docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar"
|
||||||
docker rm $CONTAINER_ID
|
docker rm $CONTAINER_ID
|
||||||
echo "Compressing arm64 rootfs..."
|
echo "Compressing arm64 rootfs..."
|
||||||
gzip firegex-rootfs-arm64.tar
|
gzip firegex-rootfs-arm64.tar
|
||||||
ls -lh firegex-rootfs-arm64.tar.gz
|
ls -lh firegex-rootfs-arm64.tar.gz
|
||||||
|
|
||||||
- name: Upload rootfs assets to release
|
- name: Upload rootfs assets to release
|
||||||
run: |
|
run: |
|
||||||
echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..."
|
echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..."
|
||||||
gh release upload ${{ steps.get_tag.outputs.tag }} \
|
gh release upload ${{ steps.get_tag.outputs.tag }} \
|
||||||
firegex-rootfs-amd64.tar.gz \
|
firegex-rootfs-amd64.tar.gz \
|
||||||
firegex-rootfs-arm64.tar.gz \
|
firegex-rootfs-arm64.tar.gz \
|
||||||
--clobber
|
--clobber
|
||||||
echo "Assets uploaded successfully!"
|
echo "Assets uploaded successfully!"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
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
|
# # This workflow will upload a Python Package using Twine when a release is created
|
||||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
# # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||||
|
|
||||||
# This workflow uses actions that are not certified by GitHub.
|
# # This workflow uses actions that are not certified by GitHub.
|
||||||
# They are provided by a third-party and are governed by
|
# # They are provided by a third-party and are governed by
|
||||||
# separate terms of service, privacy policy, and support
|
# # separate terms of service, privacy policy, and support
|
||||||
# documentation.
|
# # documentation.
|
||||||
|
|
||||||
name: Upload Python Package (fgex alias)
|
# name: Upload Python Package (fgex alias)
|
||||||
|
|
||||||
on:
|
# on:
|
||||||
release:
|
# release:
|
||||||
types:
|
# types:
|
||||||
- published
|
# - published
|
||||||
|
|
||||||
permissions:
|
# permissions:
|
||||||
contents: read
|
# contents: read
|
||||||
|
|
||||||
jobs:
|
# jobs:
|
||||||
deploy:
|
# deploy:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
# steps:
|
||||||
- uses: actions/checkout@v4
|
# - uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
# - name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
# uses: actions/setup-python@v5
|
||||||
with:
|
# with:
|
||||||
python-version: '3.x'
|
# python-version: '3.x'
|
||||||
- name: Install dependencies
|
# - name: Install dependencies
|
||||||
run: |
|
# run: |
|
||||||
python -m pip install --upgrade pip
|
# python -m pip install --upgrade pip
|
||||||
pip install build
|
# pip install build
|
||||||
- name: Extract tag name
|
# - name: Extract tag name
|
||||||
id: tag
|
# id: tag
|
||||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||||
- name: Update version in setup.py
|
# - name: Update version in setup.py
|
||||||
run: >-
|
# run: >-
|
||||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
||||||
- name: Build package
|
# - name: Build package
|
||||||
run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
# run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
||||||
- name: Publish package
|
# - name: Publish package
|
||||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||||
with:
|
# with:
|
||||||
user: __token__
|
# user: __token__
|
||||||
password: ${{ secrets.PYPI_API_TOKEN_FGEX }}
|
# password: ${{ secrets.PYPI_API_TOKEN_FGEX }}
|
||||||
|
|||||||
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
|
# # This workflow will upload a Python Package using Twine when a release is created
|
||||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
# # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||||
|
|
||||||
# This workflow uses actions that are not certified by GitHub.
|
# # This workflow uses actions that are not certified by GitHub.
|
||||||
# They are provided by a third-party and are governed by
|
# # They are provided by a third-party and are governed by
|
||||||
# separate terms of service, privacy policy, and support
|
# # separate terms of service, privacy policy, and support
|
||||||
# documentation.
|
# # documentation.
|
||||||
|
|
||||||
name: Upload Python Package
|
# name: Upload Python Package
|
||||||
|
|
||||||
on:
|
# on:
|
||||||
release:
|
# release:
|
||||||
types:
|
# types:
|
||||||
- published
|
# - published
|
||||||
|
|
||||||
permissions:
|
# permissions:
|
||||||
contents: read
|
# contents: read
|
||||||
|
|
||||||
jobs:
|
# jobs:
|
||||||
deploy:
|
# deploy:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
# steps:
|
||||||
- uses: actions/checkout@v4
|
# - uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
# - name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
# uses: actions/setup-python@v5
|
||||||
with:
|
# with:
|
||||||
python-version: '3.x'
|
# python-version: '3.x'
|
||||||
- name: Install dependencies
|
# - name: Install dependencies
|
||||||
run: |
|
# run: |
|
||||||
python -m pip install --upgrade pip
|
# python -m pip install --upgrade pip
|
||||||
pip install build
|
# pip install build
|
||||||
- name: Extract tag name
|
# - name: Extract tag name
|
||||||
id: tag
|
# id: tag
|
||||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||||
- name: Update version in setup.py
|
# - name: Update version in setup.py
|
||||||
run: >-
|
# run: >-
|
||||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||||
- name: Build package
|
# - name: Build package
|
||||||
run: cd fgex-lib && python -m build && mv ./dist ../
|
# run: cd fgex-lib && python -m build && mv ./dist ../
|
||||||
- name: Publish package
|
# - name: Publish package
|
||||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||||
with:
|
# with:
|
||||||
user: __token__
|
# user: __token__
|
||||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
# password: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
|
|||||||
112
Dockerfile
112
Dockerfile
@@ -1,49 +1,63 @@
|
|||||||
|
|
||||||
# Firegex Dockerfile UUID signature
|
# Firegex Dockerfile UUID signature
|
||||||
# cf1795af-3284-4183-a888-81ad3590ad84
|
# cf1795af-3284-4183-a888-81ad3590ad84
|
||||||
# Needed for run.py to detect the Dockerfile
|
# Needed for run.py to detect the Dockerfile
|
||||||
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM oven/bun AS frontend
|
FROM --platform=$BUILDPLATFORM oven/bun AS frontend
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ADD ./frontend/package.json .
|
ADD ./frontend/package.json .
|
||||||
ADD ./frontend/bun.lock .
|
ADD ./frontend/bun.lock .
|
||||||
RUN bun i
|
RUN bun i
|
||||||
COPY ./frontend/ .
|
COPY ./frontend/ .
|
||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
# Base fedora container
|
# Base Ubuntu container
|
||||||
FROM --platform=$TARGETARCH quay.io/fedora/fedora:42 AS base
|
FROM --platform=$TARGETARCH ubuntu:24.04 AS base
|
||||||
RUN dnf -y update && dnf install -y python3.13 libnetfilter_queue \
|
RUN apt-get update && apt-get install -y python3 libnetfilter-queue1 \
|
||||||
libnfnetlink libmnl libcap-ng-utils nftables \
|
libnfnetlink0 libmnl0 libcap-ng-utils libcap2-bin nftables \
|
||||||
vectorscan libtins python3-nftables libpcap && dnf clean all
|
libhyperscan5 python3-nftables libpcap0.8 && \
|
||||||
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
RUN mkdir -p /execute/modules
|
|
||||||
WORKDIR /execute
|
RUN mkdir -p /execute/modules
|
||||||
|
WORKDIR /execute
|
||||||
FROM --platform=$TARGETARCH base AS compiler
|
|
||||||
|
FROM --platform=$TARGETARCH base AS compiler
|
||||||
RUN dnf -y update && dnf install -y python3.13-devel @development-tools gcc-c++ \
|
|
||||||
libnetfilter_queue-devel libnfnetlink-devel libmnl-devel \
|
RUN apt-get update && apt-get install -y python3-dev build-essential g++ \
|
||||||
vectorscan-devel libtins-devel libpcap-devel boost-devel
|
libnetfilter-queue-dev libnfnetlink-dev libmnl-dev \
|
||||||
|
libhyperscan-dev libpcap-dev libboost-dev pkg-config wget cmake && \
|
||||||
COPY ./backend/binsrc /execute/binsrc
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
|
|
||||||
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.13 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
|
# 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 && \
|
||||||
#Building main conteiner
|
tar -xzf v4.5.tar.gz && cd libtins-4.5 && \
|
||||||
FROM --platform=$TARGETARCH base AS final
|
mkdir build && cd build && \
|
||||||
|
cmake ../ -DLIBTINS_ENABLE_CXX11=1 && \
|
||||||
COPY ./backend/requirements.txt /execute/requirements.txt
|
make && make install && ldconfig && \
|
||||||
COPY ./fgex-lib /execute/fgex-lib
|
cd ../.. && rm -rf libtins-4.5 v4.5.tar.gz
|
||||||
|
|
||||||
RUN dnf -y update && dnf install -y gcc-c++ python3.13-devel uv git &&\
|
COPY ./backend/binsrc /execute/binsrc
|
||||||
uv pip install --no-cache --system ./fgex-lib &&\
|
RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
|
||||||
uv pip install --no-cache --system -r /execute/requirements.txt &&\
|
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.12 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
|
||||||
uv cache clean && dnf remove -y gcc-c++ python3.13-devel uv git && dnf clean all
|
|
||||||
|
#Building main conteiner
|
||||||
COPY ./backend/ /execute/
|
FROM --platform=$TARGETARCH base AS final
|
||||||
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
|
|
||||||
COPY --from=frontend /app/dist/ ./frontend/
|
COPY ./backend/requirements.txt /execute/requirements.txt
|
||||||
|
COPY ./fgex-lib /execute/fgex-lib
|
||||||
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"]
|
|
||||||
|
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/))
|
- 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/)
|
- 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).
|
- 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
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ if __name__ == '__main__':
|
|||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"app:app",
|
"app:app",
|
||||||
# None allows to bind also on ipv6, and is selected if FIREGEX_HOST is any
|
# None allows to bind also on ipv6, and is selected if FIREGEX_HOST is any
|
||||||
host=None if FIREGEX_HOST == "any" else FIREGEX_HOST,
|
host="" if FIREGEX_HOST == "any" else FIREGEX_HOST,
|
||||||
port=FIREGEX_PORT,
|
port=FIREGEX_PORT,
|
||||||
uds=FIREGEX_SOCKET,
|
uds=FIREGEX_SOCKET,
|
||||||
reload=DEBUG and not NORELOAD,
|
reload=DEBUG and not NORELOAD,
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
chown nobody -R /execute/
|
chown nobody -R /execute/
|
||||||
|
|
||||||
|
# Create socket directory if SOCKET_DIR is set
|
||||||
|
if [ -n "$SOCKET_DIR" ]; then
|
||||||
|
mkdir -p "$SOCKET_DIR"
|
||||||
|
chown nobody:nobody "$SOCKET_DIR"
|
||||||
|
chmod 755 "$SOCKET_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[*] Attempting to start with capabilities..."
|
echo "[*] Attempting to start with capabilities..."
|
||||||
|
|
||||||
if capsh --caps="cap_net_admin,cap_setpcap,cap_setuid,cap_setgid,cap_sys_nice+eip" \
|
if capsh --caps="cap_net_admin,cap_setpcap,cap_setuid,cap_setgid,cap_sys_nice+eip" \
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import asyncio
|
|||||||
import traceback
|
import traceback
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
from utils import run_func
|
from utils import run_func
|
||||||
from utils import DEBUG
|
from utils import DEBUG
|
||||||
from utils import nicenessify
|
from utils import nicenessify
|
||||||
@@ -35,11 +36,12 @@ class FiregexInterceptor:
|
|||||||
self.last_time_exception = 0
|
self.last_time_exception = 0
|
||||||
self.outstrem_function = None
|
self.outstrem_function = None
|
||||||
self.expection_function = None
|
self.expection_function = None
|
||||||
|
self.traffic_function = None
|
||||||
self.outstrem_task: asyncio.Task
|
self.outstrem_task: asyncio.Task
|
||||||
self.outstrem_buffer = ""
|
self.outstrem_buffer = ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def start(cls, srv: Service, outstream_func=None, exception_func=None):
|
async def start(cls, srv: Service, outstream_func=None, exception_func=None, traffic_func=None):
|
||||||
self = cls()
|
self = cls()
|
||||||
self.srv = srv
|
self.srv = srv
|
||||||
self.filter_map_lock = asyncio.Lock()
|
self.filter_map_lock = asyncio.Lock()
|
||||||
@@ -47,6 +49,7 @@ class FiregexInterceptor:
|
|||||||
self.sock_conn_lock = asyncio.Lock()
|
self.sock_conn_lock = asyncio.Lock()
|
||||||
self.outstrem_function = outstream_func
|
self.outstrem_function = outstream_func
|
||||||
self.expection_function = exception_func
|
self.expection_function = exception_func
|
||||||
|
self.traffic_function = traffic_func
|
||||||
if not self.sock_conn_lock.locked():
|
if not self.sock_conn_lock.locked():
|
||||||
await self.sock_conn_lock.acquire()
|
await self.sock_conn_lock.acquire()
|
||||||
self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock"
|
self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock"
|
||||||
@@ -83,9 +86,21 @@ class FiregexInterceptor:
|
|||||||
self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n"
|
self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n"
|
||||||
if self.outstrem_function:
|
if self.outstrem_function:
|
||||||
await run_func(self.outstrem_function, self.srv.id, out_data)
|
await run_func(self.outstrem_function, self.srv.id, out_data)
|
||||||
|
# Parse JSON traffic events (if binary emits them)
|
||||||
|
if self.traffic_function:
|
||||||
|
for line in out_data.splitlines():
|
||||||
|
if line.startswith("{"): # JSON event from binary
|
||||||
|
try:
|
||||||
|
event = json.loads(line)
|
||||||
|
if "ts" in event and "verdict" in event: # Basic validation
|
||||||
|
await run_func(self.traffic_function, self.srv.id, event)
|
||||||
|
except (json.JSONDecodeError, KeyError):
|
||||||
|
pass # Ignore malformed JSON, keep backward compat with raw logs
|
||||||
|
|
||||||
async def _start_binary(self):
|
async def _start_binary(self):
|
||||||
proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))
|
proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))
|
||||||
|
# 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(
|
self.process = await asyncio.create_subprocess_exec(
|
||||||
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
|
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
@@ -93,7 +108,9 @@ class FiregexInterceptor:
|
|||||||
env={
|
env={
|
||||||
"NTHREADS": os.getenv("NTHREADS","1"),
|
"NTHREADS": os.getenv("NTHREADS","1"),
|
||||||
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
"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)
|
nicenessify(-10, self.process.pid)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
from collections import deque
|
||||||
from modules.nfproxy.firegex import FiregexInterceptor
|
from modules.nfproxy.firegex import FiregexInterceptor
|
||||||
from modules.nfproxy.nftables import FiregexTables, FiregexFilter
|
from modules.nfproxy.nftables import FiregexTables, FiregexFilter
|
||||||
from modules.nfproxy.models import Service, PyFilter
|
from modules.nfproxy.models import Service, PyFilter
|
||||||
@@ -12,7 +13,7 @@ class STATUS:
|
|||||||
nft = FiregexTables()
|
nft = FiregexTables()
|
||||||
|
|
||||||
class ServiceManager:
|
class ServiceManager:
|
||||||
def __init__(self, srv: Service, db, outstream_func=None, exception_func=None):
|
def __init__(self, srv: Service, db, outstream_func=None, exception_func=None, traffic_func=None):
|
||||||
self.srv = srv
|
self.srv = srv
|
||||||
self.db = db
|
self.db = db
|
||||||
self.status = STATUS.STOP
|
self.status = STATUS.STOP
|
||||||
@@ -21,11 +22,17 @@ class ServiceManager:
|
|||||||
self.interceptor = None
|
self.interceptor = None
|
||||||
self.outstream_function = outstream_func
|
self.outstream_function = outstream_func
|
||||||
self.last_exception_time = 0
|
self.last_exception_time = 0
|
||||||
|
self.traffic_events = deque(maxlen=500) # Ring buffer for traffic viewer
|
||||||
async def excep_internal_handler(srv, exc_time):
|
async def excep_internal_handler(srv, exc_time):
|
||||||
self.last_exception_time = exc_time
|
self.last_exception_time = exc_time
|
||||||
if exception_func:
|
if exception_func:
|
||||||
await run_func(exception_func, srv, exc_time)
|
await run_func(exception_func, srv, exc_time)
|
||||||
self.exception_function = excep_internal_handler
|
self.exception_function = excep_internal_handler
|
||||||
|
async def traffic_internal_handler(srv, event):
|
||||||
|
self.traffic_events.append(event)
|
||||||
|
if traffic_func:
|
||||||
|
await run_func(traffic_func, srv, event)
|
||||||
|
self.traffic_function = traffic_internal_handler
|
||||||
|
|
||||||
async def _update_filters_from_db(self):
|
async def _update_filters_from_db(self):
|
||||||
pyfilters = [
|
pyfilters = [
|
||||||
@@ -69,7 +76,7 @@ class ServiceManager:
|
|||||||
async def start(self):
|
async def start(self):
|
||||||
if not self.interceptor:
|
if not self.interceptor:
|
||||||
nft.delete(self.srv)
|
nft.delete(self.srv)
|
||||||
self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function, traffic_func=self.traffic_function)
|
||||||
await self._update_filters_from_db()
|
await self._update_filters_from_db()
|
||||||
self._set_status(STATUS.ACTIVE)
|
self._set_status(STATUS.ACTIVE)
|
||||||
|
|
||||||
@@ -87,14 +94,24 @@ class ServiceManager:
|
|||||||
async def update_filters(self):
|
async def update_filters(self):
|
||||||
async with self.lock:
|
async with self.lock:
|
||||||
await self._update_filters_from_db()
|
await self._update_filters_from_db()
|
||||||
|
|
||||||
|
def get_traffic_events(self, limit: int = 500):
|
||||||
|
"""Return recent traffic events from ring buffer"""
|
||||||
|
events_list = list(self.traffic_events)
|
||||||
|
return events_list[-limit:] if limit < len(events_list) else events_list
|
||||||
|
|
||||||
|
def clear_traffic_events(self):
|
||||||
|
"""Clear traffic event history"""
|
||||||
|
self.traffic_events.clear()
|
||||||
|
|
||||||
class FirewallManager:
|
class FirewallManager:
|
||||||
def __init__(self, db:SQLite, outstream_func=None, exception_func=None):
|
def __init__(self, db:SQLite, outstream_func=None, exception_func=None, traffic_func=None):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.service_table: dict[str, ServiceManager] = {}
|
self.service_table: dict[str, ServiceManager] = {}
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
self.outstream_function = outstream_func
|
self.outstream_function = outstream_func
|
||||||
self.exception_function = exception_func
|
self.exception_function = exception_func
|
||||||
|
self.traffic_function = traffic_func
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
for key in list(self.service_table.keys()):
|
for key in list(self.service_table.keys()):
|
||||||
@@ -116,7 +133,7 @@ class FirewallManager:
|
|||||||
srv = Service.from_dict(srv)
|
srv = Service.from_dict(srv)
|
||||||
if srv.id in self.service_table:
|
if srv.id in self.service_table:
|
||||||
continue
|
continue
|
||||||
self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function, traffic_func=self.traffic_function)
|
||||||
await self.service_table[srv.id].next(srv.status)
|
await self.service_table[srv.id].next(srv.status)
|
||||||
|
|
||||||
def get(self,srv_id) -> ServiceManager:
|
def get(self,srv_id) -> ServiceManager:
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ def convert_protocol_to_l4(proto:str):
|
|||||||
return "tcp"
|
return "tcp"
|
||||||
elif proto == "http":
|
elif proto == "http":
|
||||||
return "tcp"
|
return "tcp"
|
||||||
|
elif proto == "udp":
|
||||||
|
return "udp"
|
||||||
else:
|
else:
|
||||||
raise Exception("Invalid protocol")
|
raise Exception("Invalid protocol")
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
fastapi[all]
|
fastapi[all]
|
||||||
httpx
|
httpx
|
||||||
uvicorn[standard]
|
uvicorn[standard]
|
||||||
psutil
|
psutil
|
||||||
python-jose[cryptography]
|
python-jose[cryptography]
|
||||||
python-socketio
|
python-socketio
|
||||||
git+https://github.com/google/brotli.git@35d4992ac8eb1eca3b6c5f220e76cfc8b7e470aa
|
brotli
|
||||||
#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py
|
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',
|
'status': 'VARCHAR(100) NOT NULL',
|
||||||
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
|
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
|
||||||
'name': 'VARCHAR(100) NOT NULL UNIQUE',
|
'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"))',
|
'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))',
|
||||||
'ip_int': 'VARCHAR(100) NOT NULL',
|
'ip_int': 'VARCHAR(100) NOT NULL',
|
||||||
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
|
'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-outstream-leave", leave_outstream)
|
||||||
utils.socketio.on("nfproxy-exception-join", join_exception)
|
utils.socketio.on("nfproxy-exception-join", join_exception)
|
||||||
utils.socketio.on("nfproxy-exception-leave", leave_exception)
|
utils.socketio.on("nfproxy-exception-leave", leave_exception)
|
||||||
|
utils.socketio.on("nfproxy-traffic-join", join_traffic)
|
||||||
|
utils.socketio.on("nfproxy-traffic-leave", leave_traffic)
|
||||||
|
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
db.backup()
|
db.backup()
|
||||||
@@ -133,7 +135,10 @@ async def outstream_func(service_id, data):
|
|||||||
async def exception_func(service_id, timestamp):
|
async def exception_func(service_id, timestamp):
|
||||||
await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}")
|
await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}")
|
||||||
|
|
||||||
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func)
|
async def traffic_func(service_id, event):
|
||||||
|
await utils.socketio.emit(f"nfproxy-traffic-{service_id}", event, room=f"nfproxy-traffic-{service_id}")
|
||||||
|
|
||||||
|
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func, traffic_func=traffic_func)
|
||||||
|
|
||||||
@app.get('/services', response_model=list[ServiceModel])
|
@app.get('/services', response_model=list[ServiceModel])
|
||||||
async def get_service_list():
|
async def get_service_list():
|
||||||
@@ -300,7 +305,7 @@ async def add_new_service(form: ServiceAddForm):
|
|||||||
form.ip_int = ip_parse(form.ip_int)
|
form.ip_int = ip_parse(form.ip_int)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(status_code=400, detail="Invalid address")
|
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")
|
raise HTTPException(status_code=400, detail="Invalid protocol")
|
||||||
srv_id = None
|
srv_id = None
|
||||||
try:
|
try:
|
||||||
@@ -368,6 +373,28 @@ async def get_pyfilters_code(service_id: str):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@app.get('/services/{service_id}/traffic')
|
||||||
|
async def get_traffic_events(service_id: str, limit: int = 500):
|
||||||
|
"""Get recent traffic events from the service ring buffer"""
|
||||||
|
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
|
||||||
|
raise HTTPException(status_code=400, detail="This service does not exists!")
|
||||||
|
try:
|
||||||
|
events = firewall.get(service_id).get_traffic_events(limit)
|
||||||
|
return {"events": events, "count": len(events)}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.post('/services/{service_id}/traffic/clear', response_model=StatusMessageModel)
|
||||||
|
async def clear_traffic_events(service_id: str):
|
||||||
|
"""Clear traffic event history for a service"""
|
||||||
|
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
|
||||||
|
raise HTTPException(status_code=400, detail="This service does not exists!")
|
||||||
|
try:
|
||||||
|
firewall.get(service_id).clear_traffic_events()
|
||||||
|
return {"status": "ok"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
#Socket io events
|
#Socket io events
|
||||||
async def join_outstream(sid, data):
|
async def join_outstream(sid, data):
|
||||||
"""Client joins a room."""
|
"""Client joins a room."""
|
||||||
@@ -397,3 +424,20 @@ async def leave_exception(sid, data):
|
|||||||
if srv:
|
if srv:
|
||||||
await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}")
|
await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}")
|
||||||
|
|
||||||
|
async def join_traffic(sid, data):
|
||||||
|
"""Client joins traffic viewer room and gets initial event history."""
|
||||||
|
srv = data.get("service")
|
||||||
|
if srv:
|
||||||
|
room = f"nfproxy-traffic-{srv}"
|
||||||
|
await utils.socketio.enter_room(sid, room)
|
||||||
|
try:
|
||||||
|
events = firewall.get(srv).get_traffic_events(500)
|
||||||
|
await utils.socketio.emit("nfproxy-traffic-history", {"events": events}, room=sid)
|
||||||
|
except Exception:
|
||||||
|
pass # Service may not exist or not started
|
||||||
|
|
||||||
|
async def leave_traffic(sid, data):
|
||||||
|
"""Client leaves traffic viewer room."""
|
||||||
|
srv = data.get("service")
|
||||||
|
if srv:
|
||||||
|
await utils.socketio.leave_room(sid, f"nfproxy-traffic-{srv}")
|
||||||
|
|||||||
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
|
import asyncio
|
||||||
from ipaddress import ip_address, ip_interface
|
from ipaddress import ip_address, ip_interface
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import psutil
|
import psutil
|
||||||
import sys
|
import sys
|
||||||
import nftables
|
import nftables
|
||||||
from socketio import AsyncServer
|
from socketio import AsyncServer
|
||||||
from fastapi import Path
|
from fastapi import Path
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import BaseModel, ValidationError
|
||||||
import traceback
|
import traceback
|
||||||
from utils.models import StatusMessageModel
|
from utils.models import StatusMessageModel
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
|
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
|
||||||
|
|
||||||
socketio:AsyncServer = None
|
socketio:AsyncServer = None
|
||||||
sid_list:set = set()
|
sid_list:set = set()
|
||||||
|
|
||||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
|
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
|
||||||
ON_DOCKER = "DOCKER" in sys.argv
|
ON_DOCKER = "DOCKER" in sys.argv
|
||||||
DEBUG = "DEBUG" in sys.argv
|
DEBUG = "DEBUG" in sys.argv
|
||||||
NORELOAD = "NORELOAD" in sys.argv
|
NORELOAD = "NORELOAD" in sys.argv
|
||||||
FIREGEX_PORT = int(os.getenv("PORT","4444"))
|
FIREGEX_PORT = int(os.getenv("PORT","4444"))
|
||||||
FIREGEX_HOST = os.getenv("HOST","0.0.0.0")
|
FIREGEX_HOST = os.getenv("HOST","0.0.0.0")
|
||||||
FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None)
|
FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None)
|
||||||
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
|
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
|
||||||
JWT_ALGORITHM: str = "HS256"
|
JWT_ALGORITHM: str = "HS256"
|
||||||
API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
|
API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
|
||||||
|
|
||||||
PortType = Annotated[int, Path(gt=0, lt=65536)]
|
PortType = Annotated[int, Path(gt=0, lt=65536)]
|
||||||
|
|
||||||
async def run_func(func, *args, **kwargs):
|
async def run_func(func, *args, **kwargs):
|
||||||
if asyncio.iscoroutinefunction(func):
|
if asyncio.iscoroutinefunction(func):
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
async def socketio_emit(elements:list[str]):
|
async def socketio_emit(elements:list[str]):
|
||||||
await socketio.emit("update",elements)
|
await socketio.emit("update",elements)
|
||||||
|
|
||||||
def refactor_name(name:str):
|
def refactor_name(name:str):
|
||||||
name = name.strip()
|
name = name.strip()
|
||||||
while " " in name:
|
while " " in name:
|
||||||
name = name.replace(" "," ")
|
name = name.replace(" "," ")
|
||||||
return name
|
return name
|
||||||
|
|
||||||
class SysctlManager:
|
class SysctlManager:
|
||||||
def __init__(self, ctl_table):
|
def __init__(self, ctl_table):
|
||||||
self.old_table = {}
|
self.old_table = {}
|
||||||
self.new_table = {}
|
self.new_table = {}
|
||||||
if os.path.isdir("/sys_host/"):
|
if os.path.isdir("/sys_host/"):
|
||||||
self.old_table = dict()
|
self.old_table = dict()
|
||||||
self.new_table = dict(ctl_table)
|
self.new_table = dict(ctl_table)
|
||||||
for name in ctl_table.keys():
|
for name in ctl_table.keys():
|
||||||
self.old_table[name] = read_sysctl(name)
|
self.old_table[name] = read_sysctl(name)
|
||||||
|
|
||||||
def write_table(self, table) -> bool:
|
def write_table(self, table) -> bool:
|
||||||
for name, value in table.items():
|
for name, value in table.items():
|
||||||
if read_sysctl(name) != value:
|
if read_sysctl(name) != value:
|
||||||
write_sysctl(name, value)
|
write_sysctl(name, value)
|
||||||
|
|
||||||
def set(self):
|
def set(self):
|
||||||
self.write_table(self.new_table)
|
self.write_table(self.new_table)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.write_table(self.old_table)
|
self.write_table(self.old_table)
|
||||||
|
|
||||||
def read_sysctl(name:str):
|
def read_sysctl(name:str):
|
||||||
with open(f"/sys_host/{name}", "rt") as f:
|
with open(f"/sys_host/{name}", "rt") as f:
|
||||||
return "1" in f.read()
|
return "1" in f.read()
|
||||||
|
|
||||||
def write_sysctl(name:str, value:bool):
|
def write_sysctl(name:str, value:bool):
|
||||||
with open(f"/sys_host/{name}", "wt") as f:
|
with open(f"/sys_host/{name}", "wt") as f:
|
||||||
f.write("1" if value else "0")
|
f.write("1" if value else "0")
|
||||||
|
|
||||||
def list_files(mypath):
|
def list_files(mypath):
|
||||||
from os import listdir
|
from os import listdir
|
||||||
from os.path import isfile, join
|
from os.path import isfile, join
|
||||||
return [f for f in listdir(mypath) if isfile(join(mypath, f))]
|
return [f for f in listdir(mypath) if isfile(join(mypath, f))]
|
||||||
|
|
||||||
def ip_parse(ip:str):
|
def ip_parse(ip:str):
|
||||||
return str(ip_interface(ip).network)
|
return str(ip_interface(ip).network)
|
||||||
|
|
||||||
def is_ip_parse(ip:str):
|
def is_ip_parse(ip:str):
|
||||||
try:
|
try:
|
||||||
ip_parse(ip)
|
ip_parse(ip)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def addr_parse(ip:str):
|
def addr_parse(ip:str):
|
||||||
return str(ip_address(ip))
|
return str(ip_address(ip))
|
||||||
|
|
||||||
def ip_family(ip:str):
|
def ip_family(ip:str):
|
||||||
return "ip6" if ip_interface(ip).version == 6 else "ip"
|
return "ip6" if ip_interface(ip).version == 6 else "ip"
|
||||||
|
|
||||||
def get_interfaces():
|
def get_interfaces():
|
||||||
def _get_interfaces():
|
def _get_interfaces():
|
||||||
for int_name, interfs in psutil.net_if_addrs().items():
|
for int_name, interfs in psutil.net_if_addrs().items():
|
||||||
for interf in interfs:
|
for interf in interfs:
|
||||||
if interf.family in [socket.AF_INET, socket.AF_INET6]:
|
if interf.family in [socket.AF_INET, socket.AF_INET6]:
|
||||||
yield {"name": int_name, "addr":interf.address}
|
yield {"name": int_name, "addr":interf.address}
|
||||||
return list(_get_interfaces())
|
return list(_get_interfaces())
|
||||||
|
|
||||||
def nftables_int_to_json(ip_int):
|
def nftables_int_to_json(ip_int):
|
||||||
ip_int = ip_parse(ip_int)
|
ip_int = ip_parse(ip_int)
|
||||||
ip_addr = str(ip_int).split("/")[0]
|
ip_addr = str(ip_int).split("/")[0]
|
||||||
ip_addr_cidr = int(str(ip_int).split("/")[1])
|
ip_addr_cidr = int(str(ip_int).split("/")[1])
|
||||||
return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}
|
return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}
|
||||||
|
|
||||||
def nftables_json_to_int(ip_json_int):
|
def nftables_json_to_int(ip_json_int):
|
||||||
if isinstance(ip_json_int,str):
|
if isinstance(ip_json_int,str):
|
||||||
return str(ip_parse(ip_json_int))
|
return str(ip_parse(ip_json_int))
|
||||||
else:
|
else:
|
||||||
return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}'
|
return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}'
|
||||||
|
|
||||||
class Singleton(object):
|
class Singleton(object):
|
||||||
__instance = None
|
__instance = None
|
||||||
def __new__(class_, *args, **kwargs):
|
def __new__(class_, *args, **kwargs):
|
||||||
if not isinstance(class_.__instance, class_):
|
if not isinstance(class_.__instance, class_):
|
||||||
class_.__instance = object.__new__(class_, *args, **kwargs)
|
class_.__instance = object.__new__(class_, *args, **kwargs)
|
||||||
return class_.__instance
|
return class_.__instance
|
||||||
|
|
||||||
class NFTableManager(Singleton):
|
class NFTableManager(Singleton):
|
||||||
|
|
||||||
table_name = "firegex"
|
table_name = "firegex"
|
||||||
|
|
||||||
def __init__(self, init_cmd, reset_cmd):
|
def __init__(self, init_cmd, reset_cmd):
|
||||||
self.__init_cmds = init_cmd
|
self.__init_cmds = init_cmd
|
||||||
self.__reset_cmds = reset_cmd
|
self.__reset_cmds = reset_cmd
|
||||||
self.nft = nftables.Nftables()
|
self.nft = nftables.Nftables()
|
||||||
|
|
||||||
def raw_cmd(self, *cmds):
|
def raw_cmd(self, *cmds):
|
||||||
return self.nft.json_cmd({"nftables": list(cmds)})
|
return self.nft.json_cmd({"nftables": list(cmds)})
|
||||||
|
|
||||||
def cmd(self, *cmds):
|
def cmd(self, *cmds):
|
||||||
code, out, err = self.raw_cmd(*cmds)
|
code, out, err = self.raw_cmd(*cmds)
|
||||||
if code == 0:
|
if code == 0:
|
||||||
return out
|
return out
|
||||||
else:
|
else:
|
||||||
raise Exception(err)
|
raise Exception(err)
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
self.reset()
|
self.reset()
|
||||||
self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}})
|
self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}})
|
||||||
self.cmd(*self.__init_cmds)
|
self.cmd(*self.__init_cmds)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.raw_cmd(*self.__reset_cmds)
|
self.raw_cmd(*self.__reset_cmds)
|
||||||
|
|
||||||
def list_rules(self, tables = None, chains = None):
|
def list_rules(self, tables = None, chains = None):
|
||||||
for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]:
|
for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]:
|
||||||
if tables and filter["table"] not in tables:
|
if tables and filter["table"] not in tables:
|
||||||
continue
|
continue
|
||||||
if chains and filter["chain"] not in chains:
|
if chains and filter["chain"] not in chains:
|
||||||
continue
|
continue
|
||||||
yield filter
|
yield filter
|
||||||
|
|
||||||
def raw_list(self):
|
def raw_list(self):
|
||||||
return self.cmd({"list": {"ruleset": None}})["nftables"]
|
return self.cmd({"list": {"ruleset": None}})["nftables"]
|
||||||
|
|
||||||
def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"):
|
def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"):
|
||||||
res = obj.model_dump(mode=mode, exclude_unset=not unset)
|
res = obj.model_dump(mode=mode, exclude_unset=not unset)
|
||||||
if convert_keys:
|
if convert_keys:
|
||||||
for from_k, to_k in convert_keys.items():
|
for from_k, to_k in convert_keys.items():
|
||||||
if from_k in res:
|
if from_k in res:
|
||||||
res[to_k] = res.pop(from_k)
|
res[to_k] = res.pop(from_k)
|
||||||
if exclude:
|
if exclude:
|
||||||
for ele in exclude:
|
for ele in exclude:
|
||||||
if ele in res:
|
if ele in res:
|
||||||
del res[ele]
|
del res[ele]
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict:
|
def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict:
|
||||||
if isinstance(obj, list):
|
if isinstance(obj, list):
|
||||||
return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj]
|
return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj]
|
||||||
return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode)
|
return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode)
|
||||||
|
|
||||||
def register_event(sio_server: AsyncServer, event_name: str, model: BaseModel, response_model: BaseModel|None = None):
|
def register_event(sio_server: AsyncServer, event_name: str, model: BaseModel, response_model: BaseModel|None = None):
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
@sio_server.on(event_name) # Automatically registers the event
|
@sio_server.on(event_name) # Automatically registers the event
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrapper(sid, data):
|
async def wrapper(sid, data):
|
||||||
try:
|
try:
|
||||||
# Parse and validate incoming data
|
# Parse and validate incoming data
|
||||||
parsed_data = model.model_validate(data)
|
parsed_data = model.model_validate(data)
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return json_like(StatusMessageModel(status=f"Invalid {event_name} request"))
|
return json_like(StatusMessageModel(status=f"Invalid {event_name} request"))
|
||||||
|
|
||||||
# Call the original function with the parsed data
|
# Call the original function with the parsed data
|
||||||
result = await func(sid, parsed_data)
|
result = await func(sid, parsed_data)
|
||||||
# If a response model is provided, validate the output
|
# If a response model is provided, validate the output
|
||||||
if response_model:
|
if response_model:
|
||||||
try:
|
try:
|
||||||
parsed_result = response_model.model_validate(result)
|
parsed_result = response_model.model_validate(result)
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response"))
|
return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response"))
|
||||||
else:
|
else:
|
||||||
parsed_result = result
|
parsed_result = result
|
||||||
# Emit the validated result
|
# Emit the validated result
|
||||||
if parsed_result:
|
if parsed_result:
|
||||||
if isinstance(parsed_result, BaseModel):
|
if isinstance(parsed_result, BaseModel):
|
||||||
return json_like(parsed_result)
|
return json_like(parsed_result)
|
||||||
return parsed_result
|
return parsed_result
|
||||||
return wrapper
|
return wrapper
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def nicenessify(priority:int, pid:int|None=None):
|
def nicenessify(priority:int, pid:int|None=None):
|
||||||
try:
|
try:
|
||||||
pid = os.getpid() if pid is None else pid
|
pid = os.getpid() if pid is None else pid
|
||||||
ps = psutil.Process(pid)
|
ps = psutil.Process(pid)
|
||||||
if os.name == 'posix':
|
if os.name == 'posix':
|
||||||
ps.nice(priority)
|
ps.nice(priority)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
||||||
pass
|
pass
|
||||||
|
|||||||
116
docs/TRAFFIC_VIEWER.md
Normal file
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 firegex.nfproxy.internals.models import FullStreamAction, ExceptionAction
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from zstd import ZSTD_uncompress
|
import zstandard as zstd
|
||||||
import gzip
|
import gzip
|
||||||
import io
|
import io
|
||||||
import zlib
|
import zlib
|
||||||
@@ -200,7 +200,7 @@ class InternalCallbackHandler:
|
|||||||
break
|
break
|
||||||
elif enc == "zstd":
|
elif enc == "zstd":
|
||||||
try:
|
try:
|
||||||
decoding_body = ZSTD_uncompress(decoding_body)
|
decoding_body = zstd.decompress(decoding_body)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error decompressing zstd: {e}: skipping", flush=True)
|
print(f"Error decompressing zstd: {e}: skipping", flush=True)
|
||||||
decode_success = False
|
decode_success = False
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ typer
|
|||||||
pydantic>=2
|
pydantic>=2
|
||||||
typing-extensions>=4.7.1
|
typing-extensions>=4.7.1
|
||||||
pycryptodome
|
pycryptodome
|
||||||
zstd
|
|
||||||
watchfiles
|
watchfiles
|
||||||
fgex
|
fgex
|
||||||
websockets
|
websockets
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import { Firewall } from './pages/Firewall';
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import NFProxy from './pages/NFProxy';
|
import NFProxy from './pages/NFProxy';
|
||||||
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
||||||
import { useAuth } from './js/store';
|
import TrafficViewer from './pages/NFProxy/TrafficViewer';
|
||||||
|
import TrafficViewerMain from './pages/TrafficViewer';
|
||||||
|
import SetupPage from './pages/Setup';
|
||||||
|
import { useAuthStore } from './js/store';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
@@ -23,7 +26,7 @@ function App() {
|
|||||||
const [error, setError] = useState<string|null>()
|
const [error, setError] = useState<string|null>()
|
||||||
const [loadinBtn, setLoadingBtn] = useState(false);
|
const [loadinBtn, setLoadingBtn] = useState(false);
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { isAuthenticated, access_token } = useAuth()
|
const { access_token } = useAuthStore()
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
socketio.auth = { token: access_token || "" }
|
socketio.auth = { token: access_token || "" }
|
||||||
@@ -43,7 +46,7 @@ function App() {
|
|||||||
socketio.off("connect_error")
|
socketio.off("connect_error")
|
||||||
socketio.disconnect()
|
socketio.disconnect()
|
||||||
}
|
}
|
||||||
},[isAuthenticated])
|
},[access_token])
|
||||||
|
|
||||||
const getStatus = () =>{
|
const getStatus = () =>{
|
||||||
getstatus().then( res =>{
|
getstatus().then( res =>{
|
||||||
@@ -92,6 +95,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
}).catch( err => setError(err.toString()))
|
}).catch( err => setError(err.toString()))
|
||||||
setLoadingBtn(false)
|
setLoadingBtn(false)
|
||||||
|
form.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -119,12 +123,14 @@ function App() {
|
|||||||
setLoadingBtn(true)
|
setLoadingBtn(true)
|
||||||
await login(values).then(res => {
|
await login(values).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
|
queryClient.invalidateQueries()
|
||||||
setSystemStatus({...systemStatus, loggined:true})
|
setSystemStatus({...systemStatus, loggined:true})
|
||||||
}else{
|
}else{
|
||||||
setError("Login failed")
|
setError("Login failed")
|
||||||
}
|
}
|
||||||
}).catch( err => setError(err.toString()))
|
}).catch( err => setError(err.toString()))
|
||||||
setLoadingBtn(false)
|
setLoadingBtn(false)
|
||||||
|
form.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -169,9 +175,12 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
|
|||||||
</Route>
|
</Route>
|
||||||
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
|
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
|
||||||
<Route path=":srv" element={<ServiceDetailsNFProxy />} />
|
<Route path=":srv" element={<ServiceDetailsNFProxy />} />
|
||||||
|
<Route path=":srv/traffic" element={<TrafficViewer />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="traffic" element={<TrafficViewerMain />} />
|
||||||
<Route path="firewall" element={<Firewall />} />
|
<Route path="firewall" element={<Firewall />} />
|
||||||
<Route path="porthijack" element={<PortHijack />} />
|
<Route path="porthijack" element={<PortHijack />} />
|
||||||
|
<Route path="setup" element={<SetupPage />} />
|
||||||
<Route path="*" element={<HomeRedirector />} />
|
<Route path="*" element={<HomeRedirector />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -1,115 +1,115 @@
|
|||||||
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core';
|
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { RegexAddForm } from '../js/models';
|
import { RegexAddForm } from '../js/models';
|
||||||
import { b64decode, b64encode, okNotify } from '../js/utils';
|
import { b64decode, b64encode, okNotify } from '../js/utils';
|
||||||
import { ImCross } from "react-icons/im"
|
import { ImCross } from "react-icons/im"
|
||||||
import { nfregex } from './NFRegex/utils';
|
import { nfregex } from './NFRegex/utils';
|
||||||
|
|
||||||
type RegexAddInfo = {
|
type RegexAddInfo = {
|
||||||
regex:string,
|
regex:string,
|
||||||
mode:string,
|
mode:string,
|
||||||
is_case_insensitive:boolean,
|
is_case_insensitive:boolean,
|
||||||
deactive:boolean
|
deactive:boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:string }) {
|
function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:string }) {
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
regex:"",
|
regex:"",
|
||||||
mode:"C",
|
mode:"C",
|
||||||
is_case_insensitive:false,
|
is_case_insensitive:false,
|
||||||
deactive:false
|
deactive:false
|
||||||
},
|
},
|
||||||
validate:{
|
validate:{
|
||||||
regex: (value) => value !== "" ? null : "Regex is required",
|
regex: (value) => value !== "" ? null : "Regex is required",
|
||||||
mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode",
|
mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const close = () =>{
|
const close = () =>{
|
||||||
onClose()
|
onClose()
|
||||||
form.reset()
|
form.reset()
|
||||||
setError(null)
|
setError(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [submitLoading, setSubmitLoading] = useState(false)
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
const [error, setError] = useState<string|null>(null)
|
const [error, setError] = useState<string|null>(null)
|
||||||
|
|
||||||
const submitRequest = (values:RegexAddInfo) => {
|
const submitRequest = (values:RegexAddInfo) => {
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
|
|
||||||
const request:RegexAddForm = {
|
const request:RegexAddForm = {
|
||||||
is_case_sensitive: !values.is_case_insensitive,
|
is_case_sensitive: !values.is_case_insensitive,
|
||||||
service_id: service,
|
service_id: service,
|
||||||
mode: values.mode?values.mode:"B",
|
mode: values.mode?values.mode:"B",
|
||||||
regex: b64encode(values.regex),
|
regex: b64encode(values.regex),
|
||||||
active: !values.deactive
|
active: !values.deactive
|
||||||
}
|
}
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
nfregex.regexesadd(request).then( res => {
|
nfregex.regexesadd(request).then( res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`)
|
okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`)
|
||||||
}else if (res.toLowerCase() === "invalid regex"){
|
}else if (res.toLowerCase() === "invalid regex"){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
form.setFieldError("regex", "Invalid Regex")
|
form.setFieldError("regex", "Invalid Regex")
|
||||||
}else{
|
}else{
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Error: [ "+res+" ]")
|
setError("Error: [ "+res+" ]")
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <Modal size="xl" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
return <Modal size="xl" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Regex"
|
label="Regex"
|
||||||
placeholder="[A-Z0-9]{31}="
|
placeholder="[A-Z0-9]{31}="
|
||||||
{...form.getInputProps('regex')}
|
{...form.getInputProps('regex')}
|
||||||
/>
|
/>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Switch
|
<Switch
|
||||||
label="Case insensitive"
|
label="Case insensitive"
|
||||||
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
|
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
|
||||||
/>
|
/>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Switch
|
<Switch
|
||||||
label="Deactivate"
|
label="Deactivate"
|
||||||
{...form.getInputProps('deactive', { type: 'checkbox' })}
|
{...form.getInputProps('deactive', { type: 'checkbox' })}
|
||||||
/>
|
/>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Select
|
<Select
|
||||||
data={[
|
data={[
|
||||||
{ value: 'C', label: 'Client -> Server' },
|
{ value: 'C', label: 'Client -> Server' },
|
||||||
{ value: 'S', label: 'Server -> Client' },
|
{ value: 'S', label: 'Server -> Client' },
|
||||||
{ value: 'B', label: 'Both (Client <-> Server)' },
|
{ value: 'B', label: 'Both (Client <-> Server)' },
|
||||||
]}
|
]}
|
||||||
label="Choose the source of the packets to filter"
|
label="Choose the source of the packets to filter"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
{...form.getInputProps('mode')}
|
{...form.getInputProps('mode')}
|
||||||
/>
|
/>
|
||||||
<Group align="right" mt="md">
|
<Group align="right" mt="md">
|
||||||
<Button loading={submitLoading} type="submit">Add Filter</Button>
|
<Button loading={submitLoading} type="submit">Add Filter</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
|
|
||||||
{error?<>
|
{error?<>
|
||||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</Notification><Space h="md" /></>:null}
|
</Notification><Space h="md" /></>:null}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddNewRegex;
|
export default AddNewRegex;
|
||||||
|
|||||||
@@ -1,82 +1,82 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core';
|
import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core';
|
||||||
import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils';
|
import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils';
|
||||||
import { AiFillHome } from "react-icons/ai"
|
import { AiFillHome } from "react-icons/ai"
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { FaLock } from 'react-icons/fa';
|
import { FaLock } from 'react-icons/fa';
|
||||||
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
|
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
|
||||||
import { ImExit } from 'react-icons/im';
|
import { ImExit } from 'react-icons/im';
|
||||||
import ResetPasswordModal from './ResetPasswordModal';
|
import ResetPasswordModal from './ResetPasswordModal';
|
||||||
import ResetModal from './ResetModal';
|
import ResetModal from './ResetModal';
|
||||||
import { MenuDropDownWithButton } from '../MainLayout';
|
import { MenuDropDownWithButton } from '../MainLayout';
|
||||||
import { useNavbarStore } from '../../js/store';
|
import { useNavbarStore } from '../../js/store';
|
||||||
|
|
||||||
|
|
||||||
function HeaderPage(props: any) {
|
function HeaderPage(props: any) {
|
||||||
|
|
||||||
const navigator = useNavigate()
|
const navigator = useNavigate()
|
||||||
const { navOpened, toggleNav } = useNavbarStore()
|
const { navOpened, toggleNav } = useNavbarStore()
|
||||||
|
|
||||||
const logout_action = () => {
|
const logout_action = () => {
|
||||||
logout().then(r => {
|
logout().then(r => {
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}).catch(r => {
|
}).catch(r => {
|
||||||
errorNotify("Logout failed!",`Error: ${r}`)
|
errorNotify("Logout failed!",`Error: ${r}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const go_to_home = () => {
|
const go_to_home = () => {
|
||||||
navigator(`/${getMainPath()}`)
|
navigator(`/${getMainPath()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [changePasswordModal, setChangePasswordModal] = useState(false);
|
const [changePasswordModal, setChangePasswordModal] = useState(false);
|
||||||
const [resetFiregexModal, setResetFiregexModal] = useState(false);
|
const [resetFiregexModal, setResetFiregexModal] = useState(false);
|
||||||
return <AppShell.Header className="firegex__header__header" {...props}>
|
return <AppShell.Header className="firegex__header__header" {...props}>
|
||||||
<Burger
|
<Burger
|
||||||
hiddenFrom='md'
|
hiddenFrom='md'
|
||||||
ml="lg"
|
ml="lg"
|
||||||
opened={navOpened}
|
opened={navOpened}
|
||||||
className="firegex__header__navbtn"
|
className="firegex__header__navbtn"
|
||||||
onClick={toggleNav}
|
onClick={toggleNav}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center"}} ml={5}>
|
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center"}} ml={5}>
|
||||||
<Box className="firegex__header__divlogo">
|
<Box className="firegex__header__divlogo">
|
||||||
<Tooltip zIndex={0} label="Home" openDelay={1000} color="dark" position="right" >
|
<Tooltip zIndex={0} label="Home" openDelay={1000} color="dark" position="right" >
|
||||||
<Image src="/header-logo.png" alt="Firegex logo" w={50} onClick={()=>navigator("/")}/>
|
<Image src="/header-logo.png" alt="Firegex logo" w={50} onClick={()=>navigator("/")}/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
|
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
|
||||||
<Title order={2} >[Fi]*regex</Title>
|
<Title order={2} >[Fi]*regex</Title>
|
||||||
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a></p>
|
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a></p>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className="flex-spacer" />
|
<Box className="flex-spacer" />
|
||||||
|
|
||||||
<MenuDropDownWithButton>
|
<MenuDropDownWithButton>
|
||||||
<Menu.Label>Firewall Access</Menu.Label>
|
<Menu.Label>Firewall Access</Menu.Label>
|
||||||
<Menu.Item leftSection={<FaLock size={14} />} onClick={() => setChangePasswordModal(true)}>Change Password</Menu.Item>
|
<Menu.Item leftSection={<FaLock size={14} />} onClick={() => setChangePasswordModal(true)}>Change Password</Menu.Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Menu.Label>Actions</Menu.Label>
|
<Menu.Label>Actions</Menu.Label>
|
||||||
<Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item>
|
<Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item>
|
||||||
</MenuDropDownWithButton>
|
</MenuDropDownWithButton>
|
||||||
<Space w="md" />
|
<Space w="md" />
|
||||||
<Tooltip label="Home" position='bottom' color="teal">
|
<Tooltip label="Home" position='bottom' color="teal">
|
||||||
<ActionIcon color="teal" style={{marginRight:"10px"}}
|
<ActionIcon color="teal" style={{marginRight:"10px"}}
|
||||||
size="xl" radius="md" variant="filled"
|
size="xl" radius="md" variant="filled"
|
||||||
onClick={go_to_home}>
|
onClick={go_to_home}>
|
||||||
<AiFillHome size="25px" />
|
<AiFillHome size="25px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="Logout" position='bottom' color="blue">
|
<Tooltip label="Logout" position='bottom' color="blue">
|
||||||
<ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled">
|
<ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled">
|
||||||
<ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
|
<ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} />
|
<ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} />
|
||||||
<ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} />
|
<ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} />
|
||||||
<Space w="xl" />
|
<Space w="xl" />
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HeaderPage;
|
export default HeaderPage;
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core';
|
import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core';
|
||||||
import { AppShell } from '@mantine/core';
|
import { AppShell } from '@mantine/core';
|
||||||
import NavBar from './NavBar';
|
import NavBar from './NavBar';
|
||||||
import HeaderPage from './Header';
|
import HeaderPage from './Header';
|
||||||
import { getMainPath } from '../js/utils';
|
import { getMainPath } from '../js/utils';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import { useNavbarStore } from '../js/store';
|
import { useNavbarStore } from '../js/store';
|
||||||
import { HiMenu } from "react-icons/hi";
|
import { HiMenu } from "react-icons/hi";
|
||||||
|
|
||||||
|
|
||||||
function MainLayout({ children }:{ children:any }) {
|
function MainLayout({ children }:{ children:any }) {
|
||||||
const { navOpened } = useNavbarStore()
|
const { navOpened } = useNavbarStore()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if (location.pathname !== "/"){
|
if (location.pathname !== "/"){
|
||||||
sessionStorage.setItem('home_section', getMainPath())
|
sessionStorage.setItem('home_section', getMainPath())
|
||||||
}
|
}
|
||||||
},[location.pathname])
|
},[location.pathname])
|
||||||
return <AppShell
|
return <AppShell
|
||||||
header={{ height: 70 }}
|
header={{ height: 70 }}
|
||||||
navbar={{ width: 300 , breakpoint: "md", collapsed: { mobile: !navOpened } }}
|
navbar={{ width: 300 , breakpoint: "md", collapsed: { mobile: !navOpened } }}
|
||||||
p="md"
|
p="md"
|
||||||
>
|
>
|
||||||
<HeaderPage />
|
<HeaderPage />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<AppShell.Main>
|
<AppShell.Main>
|
||||||
<Container size="lg">
|
<Container size="lg">
|
||||||
{children}
|
{children}
|
||||||
</Container>
|
</Container>
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
<Space h="lg" />
|
<Space h="lg" />
|
||||||
|
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MainLayout;
|
export default MainLayout;
|
||||||
|
|
||||||
export const MenuDropDownWithButton = ({children}:{children:any}) => <Menu withArrow>
|
export const MenuDropDownWithButton = ({children}:{children:any}) => <Menu withArrow>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Tooltip label="More options" color="gray">
|
<Tooltip label="More options" color="gray">
|
||||||
<ActionIcon variant='transparent'>
|
<ActionIcon variant='transparent'>
|
||||||
<HiMenu size={24} color='#FFF'/>
|
<HiMenu size={24} color='#FFF'/>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{children}
|
{children}
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -1,139 +1,140 @@
|
|||||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||||
import { ImCross } from "react-icons/im"
|
import { ImCross } from "react-icons/im"
|
||||||
import { nfproxy, Service } from './utils';
|
import { nfproxy, Service } from './utils';
|
||||||
import PortAndInterface from '../PortAndInterface';
|
import PortAndInterface from '../PortAndInterface';
|
||||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||||
|
|
||||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||||
|
|
||||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
name: "",
|
name: "",
|
||||||
port:edit?.port??8080,
|
port:edit?.port??8080,
|
||||||
ip_int:edit?.ip_int??"",
|
ip_int:edit?.ip_int??"",
|
||||||
proto:edit?.proto??"tcp",
|
proto:edit?.proto??"tcp",
|
||||||
fail_open: edit?.fail_open??false,
|
fail_open: edit?.fail_open??false,
|
||||||
autostart: true
|
autostart: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: initialValues,
|
initialValues: initialValues,
|
||||||
validate:{
|
validate:{
|
||||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||||
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
|
proto: (value) => ["tcp","http","udp"].includes(value) ? null : "Invalid protocol",
|
||||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opened){
|
if (opened){
|
||||||
form.setInitialValues(initialValues)
|
form.setInitialValues(initialValues)
|
||||||
form.reset()
|
form.reset()
|
||||||
}
|
}
|
||||||
}, [opened])
|
}, [opened])
|
||||||
|
|
||||||
const close = () =>{
|
const close = () =>{
|
||||||
onClose()
|
onClose()
|
||||||
form.reset()
|
form.reset()
|
||||||
setError(null)
|
setError(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [submitLoading, setSubmitLoading] = useState(false)
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
const [error, setError] = useState<string|null>(null)
|
const [error, setError] = useState<string|null>(null)
|
||||||
|
|
||||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
if (edit){
|
if (edit){
|
||||||
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
}else{
|
}else{
|
||||||
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||||
if (res.status === "ok" && res.service_id){
|
if (res.status === "ok" && res.service_id){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
if (autostart) nfproxy.servicestart(res.service_id)
|
if (autostart) nfproxy.servicestart(res.service_id)
|
||||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||||
}else{
|
}else{
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Invalid request! [ "+res.status+" ]")
|
setError("Invalid request! [ "+res.status+" ]")
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
{!edit?<TextInput
|
{!edit?<TextInput
|
||||||
label="Service name"
|
label="Service name"
|
||||||
placeholder="Challenge 01"
|
placeholder="Challenge 01"
|
||||||
{...form.getInputProps('name')}
|
{...form.getInputProps('name')}
|
||||||
/>:null}
|
/>:null}
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
|
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Box>
|
<Box>
|
||||||
{!edit?<Switch
|
{!edit?<Switch
|
||||||
label="Auto-Start Service"
|
label="Auto-Start Service"
|
||||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||||
/>:null}
|
/>:null}
|
||||||
<Space h="sm" />
|
<Space h="sm" />
|
||||||
<Switch
|
<Switch
|
||||||
label={<Box className='center-flex'>
|
label={<Box className='center-flex'>
|
||||||
Enable fail-open nfqueue
|
Enable fail-open nfqueue
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label={<>
|
<Tooltip label={<>
|
||||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||||
</>}>
|
</>}>
|
||||||
<IoMdInformationCircleOutline size={15} />
|
<IoMdInformationCircleOutline size={15} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>}
|
</Box>}
|
||||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="flex-spacer"></Box>
|
<Box className="flex-spacer"></Box>
|
||||||
{edit?null:<SegmentedControl
|
{edit?null:<SegmentedControl
|
||||||
data={[
|
data={[
|
||||||
{ label: 'TCP', value: 'tcp' },
|
{ label: 'TCP', value: 'tcp' },
|
||||||
{ label: 'HTTP', value: 'http' },
|
{ label: 'HTTP', value: 'http' },
|
||||||
]}
|
{ label: 'UDP', value: 'udp' },
|
||||||
{...form.getInputProps('proto')}
|
]}
|
||||||
/>}
|
{...form.getInputProps('proto')}
|
||||||
</Box>
|
/>}
|
||||||
|
</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 justify='flex-end' mt="md" mb="sm">
|
||||||
</Group>
|
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||||
|
</Group>
|
||||||
{error?<>
|
|
||||||
<Space h="md" />
|
{error?<>
|
||||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
<Space h="md" />
|
||||||
Error: {error}
|
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||||
</Notification><Space h="md" />
|
Error: {error}
|
||||||
</>:null}
|
</Notification><Space h="md" />
|
||||||
|
</>:null}
|
||||||
</form>
|
|
||||||
</Modal>
|
</form>
|
||||||
|
</Modal>
|
||||||
}
|
|
||||||
|
}
|
||||||
export default AddEditService;
|
|
||||||
|
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] │
|
│ * port INTEGER The port of the target to proxy [default: None] [required] │
|
||||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
╭─ 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-address TEXT The address of the local server [default: None] │
|
||||||
│ --from-port INTEGER The port of the local server [default: 7474] │
|
│ --from-port INTEGER The port of the local server [default: 7474] │
|
||||||
│ -6 Use IPv6 for the connection │
|
│ -6 Use IPv6 for the connection │
|
||||||
|
|||||||
@@ -1,164 +1,164 @@
|
|||||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||||
import { nfproxy, Service, serviceQueryKey } from '../utils';
|
import { nfproxy, Service, serviceQueryKey } from '../utils';
|
||||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||||
import YesNoModal from '../../YesNoModal';
|
import YesNoModal from '../../YesNoModal';
|
||||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||||
import { BsTrashFill } from 'react-icons/bs';
|
import { BsTrashFill } from 'react-icons/bs';
|
||||||
import { BiRename } from 'react-icons/bi'
|
import { BiRename } from 'react-icons/bi'
|
||||||
import RenameForm from './RenameForm';
|
import RenameForm from './RenameForm';
|
||||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { TbPlugConnected } from "react-icons/tb";
|
import { TbPlugConnected } from "react-icons/tb";
|
||||||
import { FaFilter } from "react-icons/fa";
|
import { FaFilter } from "react-icons/fa";
|
||||||
import { IoSettingsSharp } from 'react-icons/io5';
|
import { IoSettingsSharp } from 'react-icons/io5';
|
||||||
import AddEditService from '../AddEditService';
|
import AddEditService from '../AddEditService';
|
||||||
import { FaPencilAlt } from "react-icons/fa";
|
import { FaPencilAlt } from "react-icons/fa";
|
||||||
import { ExceptionWarning } from '../ExceptionWarning';
|
import { ExceptionWarning } from '../ExceptionWarning';
|
||||||
|
|
||||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||||
|
|
||||||
let status_color = "gray";
|
let status_color = "gray";
|
||||||
switch(service.status){
|
switch(service.status){
|
||||||
case "stop": status_color = "red"; break;
|
case "stop": status_color = "red"; break;
|
||||||
case "active": status_color = "teal"; break;
|
case "active": status_color = "teal"; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [buttonLoading, setButtonLoading] = useState(false)
|
const [buttonLoading, setButtonLoading] = useState(false)
|
||||||
const [deleteModal, setDeleteModal] = useState(false)
|
const [deleteModal, setDeleteModal] = useState(false)
|
||||||
const [renameModal, setRenameModal] = useState(false)
|
const [renameModal, setRenameModal] = useState(false)
|
||||||
const [editModal, setEditModal] = useState(false)
|
const [editModal, setEditModal] = useState(false)
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
|
|
||||||
const stopService = async () => {
|
const stopService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
|
|
||||||
await nfproxy.servicestop(service.service_id).then(res => {
|
await nfproxy.servicestop(service.service_id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false);
|
setButtonLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const startService = async () => {
|
const startService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
await nfproxy.servicestart(service.service_id).then(res => {
|
await nfproxy.servicestart(service.service_id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false)
|
setButtonLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteService = () => {
|
const deleteService = () => {
|
||||||
nfproxy.servicedelete(service.service_id).then(res => {
|
nfproxy.servicedelete(service.service_id).then(res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else
|
}else
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Box className='firegex__nfregex__rowbox'>
|
<Box className='firegex__nfregex__rowbox'>
|
||||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||||
<Box>
|
<Box>
|
||||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||||
<Title className="firegex__nfregex__name" ml="xs">
|
<Title className="firegex__nfregex__name" ml="xs">
|
||||||
{service.name}
|
{service.name}
|
||||||
</Title>
|
</Title>
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||||
:{service.port}
|
:{service.port}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space w="xl" />}
|
{isMedium?null:<Space w="xl" />}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||||
<Box className="center-flex-row">
|
<Box className="center-flex-row">
|
||||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||||
<Space h="xs" />
|
<Space h="xs" />
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
|
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
|
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||||
<Box className="center-flex">
|
<Box className="center-flex">
|
||||||
<ExceptionWarning service_id={service.service_id} />
|
<ExceptionWarning service_id={service.service_id} />
|
||||||
<Space w="sm"/>
|
<Space w="sm"/>
|
||||||
<MenuDropDownWithButton>
|
<MenuDropDownWithButton>
|
||||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
</MenuDropDownWithButton>
|
</MenuDropDownWithButton>
|
||||||
<Space w="md"/>
|
<Space w="md"/>
|
||||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||||
<ActionIcon color="red" loading={buttonLoading}
|
<ActionIcon color="red" loading={buttonLoading}
|
||||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||||
disabled={service.status === "stop"}
|
disabled={service.status === "stop"}
|
||||||
aria-describedby="tooltip-stop-id">
|
aria-describedby="tooltip-stop-id">
|
||||||
<FaStop size="20px" />
|
<FaStop size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="md"/>
|
<Space w="md"/>
|
||||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||||
<FaPlay size="20px" />
|
<FaPlay size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||||
{onClick?<Box className='firegex__service_forward_btn'>
|
{onClick?<Box className='firegex__service_forward_btn'>
|
||||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||||
</Box>:null}
|
</Box>:null}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
title='Are you sure to delete this service?'
|
title='Are you sure to delete this service?'
|
||||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
|
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
|
||||||
onClose={()=>setDeleteModal(false) }
|
onClose={()=>setDeleteModal(false) }
|
||||||
action={deleteService}
|
action={deleteService}
|
||||||
opened={deleteModal}
|
opened={deleteModal}
|
||||||
/>
|
/>
|
||||||
<RenameForm
|
<RenameForm
|
||||||
onClose={()=>setRenameModal(false)}
|
onClose={()=>setRenameModal(false)}
|
||||||
opened={renameModal}
|
opened={renameModal}
|
||||||
service={service}
|
service={service}
|
||||||
/>
|
/>
|
||||||
<AddEditService
|
<AddEditService
|
||||||
opened={editModal}
|
opened={editModal}
|
||||||
onClose={()=>setEditModal(false)}
|
onClose={()=>setEditModal(false)}
|
||||||
edit={service}
|
edit={service}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,175 +1,182 @@
|
|||||||
import { PyFilter, ServerResponse } from "../../js/models"
|
import { PyFilter, ServerResponse } from "../../js/models"
|
||||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
|
||||||
export type Service = {
|
export type Service = {
|
||||||
service_id:string,
|
service_id:string,
|
||||||
name:string,
|
name:string,
|
||||||
status:string,
|
status:string,
|
||||||
port:number,
|
port:number,
|
||||||
proto: string,
|
proto: string,
|
||||||
ip_int: string,
|
ip_int: string,
|
||||||
n_filters:number,
|
n_filters:number,
|
||||||
edited_packets:number,
|
edited_packets:number,
|
||||||
blocked_packets:number,
|
blocked_packets:number,
|
||||||
fail_open:boolean,
|
fail_open:boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAddForm = {
|
export type ServiceAddForm = {
|
||||||
name:string,
|
name:string,
|
||||||
port:number,
|
port:number,
|
||||||
proto:string,
|
proto:string,
|
||||||
ip_int:string,
|
ip_int:string,
|
||||||
fail_open: boolean,
|
fail_open: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceSettings = {
|
export type ServiceSettings = {
|
||||||
port?:number,
|
port?:number,
|
||||||
ip_int?:string,
|
ip_int?:string,
|
||||||
fail_open?: boolean,
|
fail_open?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAddResponse = {
|
export type ServiceAddResponse = {
|
||||||
status: string,
|
status: string,
|
||||||
service_id?: string,
|
service_id?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serviceQueryKey = ["nfproxy","services"]
|
export const serviceQueryKey = ["nfproxy","services"]
|
||||||
|
|
||||||
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
|
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
|
||||||
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
|
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
|
||||||
queryKey:[...serviceQueryKey,service_id,"pyfilters"],
|
queryKey:[...serviceQueryKey,service_id,"pyfilters"],
|
||||||
queryFn:() => nfproxy.servicepyfilters(service_id)
|
queryFn:() => nfproxy.servicepyfilters(service_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
|
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
|
||||||
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
|
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
|
||||||
queryFn:() => nfproxy.getpyfilterscode(service_id)
|
queryFn:() => nfproxy.getpyfilterscode(service_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const nfproxy = {
|
export const nfproxy = {
|
||||||
services: async () => {
|
services: async () => {
|
||||||
return await getapi("nfproxy/services") as Service[];
|
return await getapi("nfproxy/services") as Service[];
|
||||||
},
|
},
|
||||||
serviceinfo: async (service_id:string) => {
|
serviceinfo: async (service_id:string) => {
|
||||||
return await getapi(`nfproxy/services/${service_id}`) as Service;
|
return await getapi(`nfproxy/services/${service_id}`) as Service;
|
||||||
},
|
},
|
||||||
pyfilterenable: async (service_id:string, filter_name:string) => {
|
pyfilterenable: async (service_id:string, filter_name:string) => {
|
||||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse;
|
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
pyfilterdisable: async (service_id:string, filter_name:string) => {
|
pyfilterdisable: async (service_id:string, filter_name:string) => {
|
||||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse;
|
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicestart: async (service_id:string) => {
|
servicestart: async (service_id:string) => {
|
||||||
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
|
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicerename: async (service_id:string, name: string) => {
|
servicerename: async (service_id:string, name: string) => {
|
||||||
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
|
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicestop: async (service_id:string) => {
|
servicestop: async (service_id:string) => {
|
||||||
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
|
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicesadd: async (data:ServiceAddForm) => {
|
servicesadd: async (data:ServiceAddForm) => {
|
||||||
return await postapi("nfproxy/services",data) as ServiceAddResponse;
|
return await postapi("nfproxy/services",data) as ServiceAddResponse;
|
||||||
},
|
},
|
||||||
servicedelete: async (service_id:string) => {
|
servicedelete: async (service_id:string) => {
|
||||||
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
|
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicepyfilters: async (service_id:string) => {
|
servicepyfilters: async (service_id:string) => {
|
||||||
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
|
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
|
||||||
},
|
},
|
||||||
settings: async (service_id:string, data:ServiceSettings) => {
|
settings: async (service_id:string, data:ServiceSettings) => {
|
||||||
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
getpyfilterscode: async (service_id:string) => {
|
getpyfilterscode: async (service_id:string) => {
|
||||||
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
||||||
},
|
},
|
||||||
setpyfilterscode: async (service_id:string, code:string) => {
|
setpyfilterscode: async (service_id:string, code:string) => {
|
||||||
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
|
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
}
|
},
|
||||||
}
|
gettraffic: async (service_id:string, limit:number = 500) => {
|
||||||
|
return await getapi(`nfproxy/services/${service_id}/traffic?limit=${limit}`) as { events: any[], count: number };
|
||||||
|
},
|
||||||
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
|
cleartraffic: async (service_id:string) => {
|
||||||
|
const { status } = await postapi(`nfproxy/services/${service_id}/traffic/clear`) as ServerResponse;
|
||||||
# From here we can import the DataTypes that we want to use:
|
return status === "ok"?undefined:status
|
||||||
# The data type must be specified in the filter functions
|
}
|
||||||
# And will also interally be used to decide when call some filters and how aggregate data
|
}
|
||||||
from firegex.nfproxy.models import RawPacket
|
|
||||||
|
|
||||||
# global context in this execution is dedicated to a single TCP stream
|
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
|
||||||
# - This code will be executed once at the TCP stream start
|
|
||||||
# - The filter will be called for each packet in the stream
|
# From here we can import the DataTypes that we want to use:
|
||||||
# - You can store in global context some data you need, but exceeding with data stored could be dangerous
|
# The data type must be specified in the filter functions
|
||||||
# - At the end of the stream the global context will be destroyed
|
# And will also interally be used to decide when call some filters and how aggregate data
|
||||||
|
from firegex.nfproxy.models import RawPacket
|
||||||
from firegex.nfproxy import pyfilter
|
|
||||||
# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
|
# global context in this execution is dedicated to a single TCP stream
|
||||||
|
# - This code will be executed once at the TCP stream start
|
||||||
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
|
# - The filter will be called for each packet in the stream
|
||||||
# - The filter must return one of the following values:
|
# - You can store in global context some data you need, but exceeding with data stored could be dangerous
|
||||||
# - ACCEPT: The packet will be accepted
|
# - At the end of the stream the global context will be destroyed
|
||||||
# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream)
|
|
||||||
# - UNSTABLE_MANGLE: The packet will be mangled and accepted
|
from firegex.nfproxy import pyfilter
|
||||||
# - DROP: All the packets in this stream will be easly dropped
|
# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
|
||||||
|
|
||||||
# If you want, you can use print to debug your filters, but this could slow down the filter
|
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
|
||||||
|
# - The filter must return one of the following values:
|
||||||
# Filter names must be unique and are specified by the name of the function wrapped by the decorator
|
# - ACCEPT: The packet will be accepted
|
||||||
@pyfilter
|
# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream)
|
||||||
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
|
# - UNSTABLE_MANGLE: The packet will be mangled and accepted
|
||||||
def strange_filter(packet:RawPacket):
|
# - DROP: All the packets in this stream will be easly dropped
|
||||||
# Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
|
|
||||||
# Also is not garanteed that l4_data is the same of the packet data:
|
# If you want, you can use print to debug your filters, but this could slow down the filter
|
||||||
# packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
|
|
||||||
# Unorder packets in TCP are accepted by default, and python is not called in this case
|
# Filter names must be unique and are specified by the name of the function wrapped by the decorator
|
||||||
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
|
@pyfilter
|
||||||
if b"TEST_MANGLING" in packet.l4_data:
|
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
|
||||||
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
|
def strange_filter(packet:RawPacket):
|
||||||
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
|
# Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
|
||||||
return UNSTABLE_MANGLE
|
# Also is not garanteed that l4_data is the same of the packet data:
|
||||||
# Drops the traffic
|
# packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
|
||||||
if b"BAD DATA 1" in packet.data:
|
# Unorder packets in TCP are accepted by default, and python is not called in this case
|
||||||
return DROP
|
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
|
||||||
# Rejects the traffic
|
if b"TEST_MANGLING" in packet.l4_data:
|
||||||
if b"BAD DATA 2" in packet.data:
|
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
|
||||||
return REJECT
|
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
|
||||||
# Accepts the traffic (default if None is returned)
|
return UNSTABLE_MANGLE
|
||||||
return ACCEPT
|
# Drops the traffic
|
||||||
|
if b"BAD DATA 1" in packet.data:
|
||||||
# Example with a higher level of abstraction
|
return DROP
|
||||||
@pyfilter
|
# Rejects the traffic
|
||||||
def http_filter(http:HTTPRequest):
|
if b"BAD DATA 2" in packet.data:
|
||||||
if http.method == "GET" and "test" in http.url:
|
return REJECT
|
||||||
return REJECT
|
# Accepts the traffic (default if None is returned)
|
||||||
|
return ACCEPT
|
||||||
# ADVANCED OPTIONS
|
|
||||||
# You can specify some additional options on the streaming managment
|
# Example with a higher level of abstraction
|
||||||
# pyproxy will automatically store all the packets (already ordered by the c++ binary):
|
@pyfilter
|
||||||
#
|
def http_filter(http:HTTPRequest):
|
||||||
# If the stream is too big, you can specify what actions to take:
|
if http.method == "GET" and "test" in http.url:
|
||||||
# This can be done defining some variables in the global context
|
return REJECT
|
||||||
# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB)
|
|
||||||
# NOTE: the stream size is calculated and managed indipendently by the data type handling system
|
# ADVANCED OPTIONS
|
||||||
# Only types required by at least 1 filter will be stored.
|
# You can specify some additional options on the streaming managment
|
||||||
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
|
# pyproxy will automatically store all the packets (already ordered by the c++ binary):
|
||||||
# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
|
#
|
||||||
# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
|
# If the stream is too big, you can specify what actions to take:
|
||||||
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
|
# This can be done defining some variables in the global context
|
||||||
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic
|
# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB)
|
||||||
|
# NOTE: the stream size is calculated and managed indipendently by the data type handling system
|
||||||
from firege.nfproxy import FullStreamAction
|
# Only types required by at least 1 filter will be stored.
|
||||||
|
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
|
||||||
# Example of a global context
|
# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
|
||||||
FGEX_STREAM_MAX_SIZE = 4096
|
# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
|
||||||
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT
|
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
|
||||||
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic
|
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic
|
||||||
`
|
|
||||||
|
from firege.nfproxy import FullStreamAction
|
||||||
|
|
||||||
|
# Example of a global context
|
||||||
|
FGEX_STREAM_MAX_SIZE = 4096
|
||||||
|
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT
|
||||||
|
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic
|
||||||
|
`
|
||||||
|
|||||||
@@ -1,139 +1,139 @@
|
|||||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||||
import { ImCross } from "react-icons/im"
|
import { ImCross } from "react-icons/im"
|
||||||
import { nfregex, Service } from './utils';
|
import { nfregex, Service } from './utils';
|
||||||
import PortAndInterface from '../PortAndInterface';
|
import PortAndInterface from '../PortAndInterface';
|
||||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||||
|
|
||||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||||
|
|
||||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
name: "",
|
name: "",
|
||||||
port:edit?.port??8080,
|
port:edit?.port??8080,
|
||||||
ip_int:edit?.ip_int??"",
|
ip_int:edit?.ip_int??"",
|
||||||
proto:edit?.proto??"tcp",
|
proto:edit?.proto??"tcp",
|
||||||
fail_open: edit?.fail_open??false,
|
fail_open: edit?.fail_open??false,
|
||||||
autostart: true
|
autostart: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: initialValues,
|
initialValues: initialValues,
|
||||||
validate:{
|
validate:{
|
||||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opened){
|
if (opened){
|
||||||
form.setInitialValues(initialValues)
|
form.setInitialValues(initialValues)
|
||||||
form.reset()
|
form.reset()
|
||||||
}
|
}
|
||||||
}, [opened])
|
}, [opened])
|
||||||
|
|
||||||
const close = () =>{
|
const close = () =>{
|
||||||
onClose()
|
onClose()
|
||||||
form.reset()
|
form.reset()
|
||||||
setError(null)
|
setError(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [submitLoading, setSubmitLoading] = useState(false)
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
const [error, setError] = useState<string|null>(null)
|
const [error, setError] = useState<string|null>(null)
|
||||||
|
|
||||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
if (edit){
|
if (edit){
|
||||||
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
|
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
}else{
|
}else{
|
||||||
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||||
if (res.status === "ok" && res.service_id){
|
if (res.status === "ok" && res.service_id){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
if (autostart) nfregex.servicestart(res.service_id)
|
if (autostart) nfregex.servicestart(res.service_id)
|
||||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||||
}else{
|
}else{
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Invalid request! [ "+res.status+" ]")
|
setError("Invalid request! [ "+res.status+" ]")
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
{!edit?<TextInput
|
{!edit?<TextInput
|
||||||
label="Service name"
|
label="Service name"
|
||||||
placeholder="Challenge 01"
|
placeholder="Challenge 01"
|
||||||
{...form.getInputProps('name')}
|
{...form.getInputProps('name')}
|
||||||
/>:null}
|
/>:null}
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
|
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Box>
|
<Box>
|
||||||
{!edit?<Switch
|
{!edit?<Switch
|
||||||
label="Auto-Start Service"
|
label="Auto-Start Service"
|
||||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||||
/>:null}
|
/>:null}
|
||||||
<Space h="sm" />
|
<Space h="sm" />
|
||||||
<Switch
|
<Switch
|
||||||
label={<Box className='center-flex'>
|
label={<Box className='center-flex'>
|
||||||
Enable fail-open nfqueue
|
Enable fail-open nfqueue
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label={<>
|
<Tooltip label={<>
|
||||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||||
</>}>
|
</>}>
|
||||||
<IoMdInformationCircleOutline size={15} />
|
<IoMdInformationCircleOutline size={15} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>}
|
</Box>}
|
||||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="flex-spacer"></Box>
|
<Box className="flex-spacer"></Box>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
data={[
|
data={[
|
||||||
{ label: 'TCP', value: 'tcp' },
|
{ label: 'TCP', value: 'tcp' },
|
||||||
{ label: 'UDP', value: 'udp' },
|
{ label: 'UDP', value: 'udp' },
|
||||||
]}
|
]}
|
||||||
{...form.getInputProps('proto')}
|
{...form.getInputProps('proto')}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group justify='flex-end' mt="md" mb="sm">
|
<Group justify='flex-end' mt="md" mb="sm">
|
||||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{error?<>
|
{error?<>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</Notification><Space h="md" />
|
</Notification><Space h="md" />
|
||||||
</>:null}
|
</>:null}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddEditService;
|
export default AddEditService;
|
||||||
|
|||||||
@@ -1,158 +1,158 @@
|
|||||||
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||||
import { nfregex, Service, serviceQueryKey } from '../utils';
|
import { nfregex, Service, serviceQueryKey } from '../utils';
|
||||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||||
import YesNoModal from '../../YesNoModal';
|
import YesNoModal from '../../YesNoModal';
|
||||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||||
import { BsTrashFill } from 'react-icons/bs';
|
import { BsTrashFill } from 'react-icons/bs';
|
||||||
import { BiRename } from 'react-icons/bi'
|
import { BiRename } from 'react-icons/bi'
|
||||||
import RenameForm from './RenameForm';
|
import RenameForm from './RenameForm';
|
||||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { FaFilter } from "react-icons/fa";
|
import { FaFilter } from "react-icons/fa";
|
||||||
import { VscRegex } from "react-icons/vsc";
|
import { VscRegex } from "react-icons/vsc";
|
||||||
import { IoSettingsSharp } from 'react-icons/io5';
|
import { IoSettingsSharp } from 'react-icons/io5';
|
||||||
import AddEditService from '../AddEditService';
|
import AddEditService from '../AddEditService';
|
||||||
|
|
||||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||||
|
|
||||||
let status_color = "gray";
|
let status_color = "gray";
|
||||||
switch(service.status){
|
switch(service.status){
|
||||||
case "stop": status_color = "red"; break;
|
case "stop": status_color = "red"; break;
|
||||||
case "active": status_color = "teal"; break;
|
case "active": status_color = "teal"; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [buttonLoading, setButtonLoading] = useState(false)
|
const [buttonLoading, setButtonLoading] = useState(false)
|
||||||
const [deleteModal, setDeleteModal] = useState(false)
|
const [deleteModal, setDeleteModal] = useState(false)
|
||||||
const [renameModal, setRenameModal] = useState(false)
|
const [renameModal, setRenameModal] = useState(false)
|
||||||
const [editModal, setEditModal] = useState(false)
|
const [editModal, setEditModal] = useState(false)
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
|
|
||||||
const stopService = async () => {
|
const stopService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
|
|
||||||
await nfregex.servicestop(service.service_id).then(res => {
|
await nfregex.servicestop(service.service_id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false);
|
setButtonLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const startService = async () => {
|
const startService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
await nfregex.servicestart(service.service_id).then(res => {
|
await nfregex.servicestart(service.service_id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false)
|
setButtonLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteService = () => {
|
const deleteService = () => {
|
||||||
nfregex.servicedelete(service.service_id).then(res => {
|
nfregex.servicedelete(service.service_id).then(res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else
|
}else
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Box className='firegex__nfregex__rowbox'>
|
<Box className='firegex__nfregex__rowbox'>
|
||||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||||
<Box>
|
<Box>
|
||||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||||
<Title className="firegex__nfregex__name" ml="xs">
|
<Title className="firegex__nfregex__name" ml="xs">
|
||||||
{service.name}
|
{service.name}
|
||||||
</Title>
|
</Title>
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||||
:{service.port}
|
:{service.port}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space w="xl" />}
|
{isMedium?null:<Space w="xl" />}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||||
<Box className="center-flex-row">
|
<Box className="center-flex-row">
|
||||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||||
<Space h="xs" />
|
<Space h="xs" />
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
|
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||||
<Box className="center-flex">
|
<Box className="center-flex">
|
||||||
<MenuDropDownWithButton>
|
<MenuDropDownWithButton>
|
||||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
</MenuDropDownWithButton>
|
</MenuDropDownWithButton>
|
||||||
<Space w="md"/>
|
<Space w="md"/>
|
||||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||||
<ActionIcon color="red" loading={buttonLoading}
|
<ActionIcon color="red" loading={buttonLoading}
|
||||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||||
disabled={service.status === "stop"}
|
disabled={service.status === "stop"}
|
||||||
aria-describedby="tooltip-stop-id">
|
aria-describedby="tooltip-stop-id">
|
||||||
<FaStop size="20px" />
|
<FaStop size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="md"/>
|
<Space w="md"/>
|
||||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||||
<FaPlay size="20px" />
|
<FaPlay size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||||
{onClick?<Box className='firegex__service_forward_btn'>
|
{onClick?<Box className='firegex__service_forward_btn'>
|
||||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||||
</Box>:null}
|
</Box>:null}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
title='Are you sure to delete this service?'
|
title='Are you sure to delete this service?'
|
||||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||||
onClose={()=>setDeleteModal(false) }
|
onClose={()=>setDeleteModal(false) }
|
||||||
action={deleteService}
|
action={deleteService}
|
||||||
opened={deleteModal}
|
opened={deleteModal}
|
||||||
/>
|
/>
|
||||||
<RenameForm
|
<RenameForm
|
||||||
onClose={()=>setRenameModal(false)}
|
onClose={()=>setRenameModal(false)}
|
||||||
opened={renameModal}
|
opened={renameModal}
|
||||||
service={service}
|
service={service}
|
||||||
/>
|
/>
|
||||||
<AddEditService
|
<AddEditService
|
||||||
opened={editModal}
|
opened={editModal}
|
||||||
onClose={()=>setEditModal(false)}
|
onClose={()=>setEditModal(false)}
|
||||||
edit={service}
|
edit={service}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,95 +1,95 @@
|
|||||||
import { RegexFilter, ServerResponse } from "../../js/models"
|
import { RegexFilter, ServerResponse } from "../../js/models"
|
||||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||||
import { RegexAddForm } from "../../js/models"
|
import { RegexAddForm } from "../../js/models"
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
|
|
||||||
export type Service = {
|
export type Service = {
|
||||||
name:string,
|
name:string,
|
||||||
service_id:string,
|
service_id:string,
|
||||||
status:string,
|
status:string,
|
||||||
port:number,
|
port:number,
|
||||||
proto: string,
|
proto: string,
|
||||||
ip_int: string,
|
ip_int: string,
|
||||||
n_packets:number,
|
n_packets:number,
|
||||||
n_regex:number,
|
n_regex:number,
|
||||||
fail_open:boolean,
|
fail_open:boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAddForm = {
|
export type ServiceAddForm = {
|
||||||
name:string,
|
name:string,
|
||||||
port:number,
|
port:number,
|
||||||
proto:string,
|
proto:string,
|
||||||
ip_int:string,
|
ip_int:string,
|
||||||
fail_open: boolean,
|
fail_open: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceSettings = {
|
export type ServiceSettings = {
|
||||||
port?:number,
|
port?:number,
|
||||||
proto?:string,
|
proto?:string,
|
||||||
ip_int?:string,
|
ip_int?:string,
|
||||||
fail_open?: boolean,
|
fail_open?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAddResponse = {
|
export type ServiceAddResponse = {
|
||||||
status: string,
|
status: string,
|
||||||
service_id?: string,
|
service_id?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serviceQueryKey = ["nfregex","services"]
|
export const serviceQueryKey = ["nfregex","services"]
|
||||||
|
|
||||||
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
|
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
|
||||||
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
|
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
|
||||||
queryKey:[...serviceQueryKey,service_id,"regexes"],
|
queryKey:[...serviceQueryKey,service_id,"regexes"],
|
||||||
queryFn:() => nfregex.serviceregexes(service_id)
|
queryFn:() => nfregex.serviceregexes(service_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const nfregex = {
|
export const nfregex = {
|
||||||
services: async () => {
|
services: async () => {
|
||||||
return await getapi("nfregex/services") as Service[];
|
return await getapi("nfregex/services") as Service[];
|
||||||
},
|
},
|
||||||
serviceinfo: async (service_id:string) => {
|
serviceinfo: async (service_id:string) => {
|
||||||
return await getapi(`nfregex/services/${service_id}`) as Service;
|
return await getapi(`nfregex/services/${service_id}`) as Service;
|
||||||
},
|
},
|
||||||
regexdelete: async (regex_id:number) => {
|
regexdelete: async (regex_id:number) => {
|
||||||
const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse;
|
const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
regexenable: async (regex_id:number) => {
|
regexenable: async (regex_id:number) => {
|
||||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse;
|
const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
regexdisable: async (regex_id:number) => {
|
regexdisable: async (regex_id:number) => {
|
||||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse;
|
const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicestart: async (service_id:string) => {
|
servicestart: async (service_id:string) => {
|
||||||
const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse;
|
const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicerename: async (service_id:string, name: string) => {
|
servicerename: async (service_id:string, name: string) => {
|
||||||
const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse;
|
const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicestop: async (service_id:string) => {
|
servicestop: async (service_id:string) => {
|
||||||
const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse;
|
const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicesadd: async (data:ServiceAddForm) => {
|
servicesadd: async (data:ServiceAddForm) => {
|
||||||
return await postapi("nfregex/services",data) as ServiceAddResponse;
|
return await postapi("nfregex/services",data) as ServiceAddResponse;
|
||||||
},
|
},
|
||||||
servicedelete: async (service_id:string) => {
|
servicedelete: async (service_id:string) => {
|
||||||
const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse;
|
const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
regexesadd: async (data:RegexAddForm) => {
|
regexesadd: async (data:RegexAddForm) => {
|
||||||
const { status } = await postapi("nfregex/regexes",data) as ServerResponse;
|
const { status } = await postapi("nfregex/regexes",data) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
serviceregexes: async (service_id:string) => {
|
serviceregexes: async (service_id:string) => {
|
||||||
return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[];
|
return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[];
|
||||||
},
|
},
|
||||||
settings: async (service_id:string, data:ServiceSettings) => {
|
settings: async (service_id:string, data:ServiceSettings) => {
|
||||||
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -7,16 +7,17 @@ import { PiWallLight } from "react-icons/pi";
|
|||||||
import { useNavbarStore } from "../../js/store";
|
import { useNavbarStore } from "../../js/store";
|
||||||
import { getMainPath } from "../../js/utils";
|
import { getMainPath } from "../../js/utils";
|
||||||
import { BsRegex } from "react-icons/bs";
|
import { BsRegex } from "react-icons/bs";
|
||||||
|
import { MdVisibility, MdSettings } from "react-icons/md";
|
||||||
|
|
||||||
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
||||||
{ navigate?: string, closeNav: () => void, name:string, icon:any, color:MantineColor, disabled?:boolean, onClick?:CallableFunction }) {
|
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
|
||||||
const navigator = useNavigate()
|
const navigator = useNavigate()
|
||||||
return <UnstyledButton
|
return <UnstyledButton
|
||||||
className={`firegex__navbar__unstyled_button${navigate==getMainPath()?" selected":""}${disabled?" disabled":""}`}
|
className={`firegex__navbar__unstyled_button${navigate == getMainPath() ? " selected" : ""}${disabled ? " disabled" : ""}`}
|
||||||
onClick={()=>{
|
onClick={() => {
|
||||||
if(navigate){navigator(`/${navigate}`);closeNav()}
|
if (navigate) { navigator(`/${navigate}`); closeNav() }
|
||||||
if (onClick) onClick()
|
if (onClick) onClick()
|
||||||
}} disabled={disabled}>
|
}} disabled={disabled}>
|
||||||
<Group>
|
<Group>
|
||||||
<ThemeIcon color={color} variant="light">
|
<ThemeIcon color={color} variant="light">
|
||||||
{icon}
|
{icon}
|
||||||
@@ -24,7 +25,7 @@ function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick
|
|||||||
<Text size="sm">{name}</Text>
|
<Text size="sm">{name}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NavBar() {
|
export default function NavBar() {
|
||||||
const [toggle, setToggleState] = useState(false);
|
const [toggle, setToggleState] = useState(false);
|
||||||
@@ -35,17 +36,19 @@ export default function NavBar() {
|
|||||||
<Title order={4}>Options ⚙️</Title>
|
<Title order={4}>Options ⚙️</Title>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider my="xs" />
|
<Divider my="xs" />
|
||||||
<Box style={{flexGrow: 1}} component={ScrollArea} px="xs" mt="xs">
|
<Box style={{ flexGrow: 1 }} component={ScrollArea} px="xs" mt="xs">
|
||||||
<NavBarButton navigate="nfregex" closeNav={closeNav} name="Netfilter Regex" color="grape" icon={<BsRegex size={19} />} />
|
<NavBarButton navigate="nfregex" closeNav={closeNav} name="Netfilter Regex" color="grape" icon={<BsRegex size={19} />} />
|
||||||
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
|
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
|
||||||
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
|
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
|
||||||
<Box px="xs" mt="lg">
|
<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>
|
<Title order={5}>Experimental Features 🧪</Title>
|
||||||
</Box>
|
</Box>
|
||||||
<Text></Text>
|
<Text></Text>
|
||||||
<Divider my="xs" />
|
<Divider my="xs" /> */}
|
||||||
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</AppShell.Navbar>
|
</AppShell.Navbar>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +1,113 @@
|
|||||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core';
|
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils';
|
import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils';
|
||||||
import { ImCross } from "react-icons/im"
|
import { ImCross } from "react-icons/im"
|
||||||
import { porthijack } from './utils';
|
import { porthijack } from './utils';
|
||||||
import PortAndInterface from '../PortAndInterface';
|
import PortAndInterface from '../PortAndInterface';
|
||||||
|
|
||||||
type ServiceAddForm = {
|
type ServiceAddForm = {
|
||||||
name:string,
|
name:string,
|
||||||
public_port:number,
|
public_port:number,
|
||||||
proxy_port:number,
|
proxy_port:number,
|
||||||
proto:string,
|
proto:string,
|
||||||
ip_src:string,
|
ip_src:string,
|
||||||
ip_dst:string,
|
ip_dst:string,
|
||||||
autostart: boolean,
|
autostart: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) {
|
function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) {
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name:"",
|
name:"",
|
||||||
public_port:80,
|
public_port:80,
|
||||||
proxy_port:8080,
|
proxy_port:8080,
|
||||||
proto:"tcp",
|
proto:"tcp",
|
||||||
ip_src:"",
|
ip_src:"",
|
||||||
ip_dst:"127.0.0.1",
|
ip_dst:"127.0.0.1",
|
||||||
autostart: false,
|
autostart: false,
|
||||||
},
|
},
|
||||||
validate:{
|
validate:{
|
||||||
name: (value) => value !== ""? null : "Service name is required",
|
name: (value) => value !== ""? null : "Service name is required",
|
||||||
public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port",
|
public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port",
|
||||||
proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port",
|
proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port",
|
||||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||||
ip_src: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid source IP address",
|
ip_src: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid source IP address",
|
||||||
ip_dst: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid destination IP address",
|
ip_dst: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid destination IP address",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const close = () =>{
|
const close = () =>{
|
||||||
onClose()
|
onClose()
|
||||||
form.reset()
|
form.reset()
|
||||||
setError(null)
|
setError(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [submitLoading, setSubmitLoading] = useState(false)
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
const [error, setError] = useState<string|null>(null)
|
const [error, setError] = useState<string|null>(null)
|
||||||
|
|
||||||
const submitRequest = ({ name, proxy_port, public_port, autostart, proto, ip_src, ip_dst }:ServiceAddForm) =>{
|
const submitRequest = ({ name, proxy_port, public_port, autostart, proto, ip_src, ip_dst }:ServiceAddForm) =>{
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).then( res => {
|
porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).then( res => {
|
||||||
if (res.status === "ok" && res.service_id){
|
if (res.status === "ok" && res.service_id){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
if (autostart) porthijack.servicestart(res.service_id)
|
if (autostart) porthijack.servicestart(res.service_id)
|
||||||
okNotify(`Service ${name} has been added`, `Successfully added service from port ${public_port} to ${proxy_port}`)
|
okNotify(`Service ${name} has been added`, `Successfully added service from port ${public_port} to ${proxy_port}`)
|
||||||
}else{
|
}else{
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Invalid request! [ "+res.status+" ]")
|
setError("Invalid request! [ "+res.status+" ]")
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
return <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Service name"
|
label="Service name"
|
||||||
placeholder="Challenge 01"
|
placeholder="Challenge 01"
|
||||||
{...form.getInputProps('name')}
|
{...form.getInputProps('name')}
|
||||||
/>
|
/>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" />
|
<PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" />
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" />
|
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" />
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
|
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Switch
|
<Switch
|
||||||
label="Auto-Start Service"
|
label="Auto-Start Service"
|
||||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||||
/>
|
/>
|
||||||
<Box className="flex-spacer" />
|
<Box className="flex-spacer" />
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
data={[
|
data={[
|
||||||
{ label: 'TCP', value: 'tcp' },
|
{ label: 'TCP', value: 'tcp' },
|
||||||
{ label: 'UDP', value: 'udp' },
|
{ label: 'UDP', value: 'udp' },
|
||||||
]}
|
]}
|
||||||
{...form.getInputProps('proto')}
|
{...form.getInputProps('proto')}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group justify='flex-end' mt="md" mb="sm">
|
<Group justify='flex-end' mt="md" mb="sm">
|
||||||
<Button loading={submitLoading} type="submit">Add Service</Button>
|
<Button loading={submitLoading} type="submit">Add Service</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{error?<>
|
{error?<>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</Notification><Space h="md" />
|
</Notification><Space h="md" />
|
||||||
</>:null}
|
</>:null}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddNewService;
|
export default AddNewService;
|
||||||
|
|||||||
@@ -1,152 +1,152 @@
|
|||||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||||
import { porthijack, Service } from '../utils';
|
import { porthijack, Service } from '../utils';
|
||||||
import YesNoModal from '../../YesNoModal';
|
import YesNoModal from '../../YesNoModal';
|
||||||
import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils';
|
import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils';
|
||||||
import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs';
|
import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs';
|
||||||
import { BiRename } from 'react-icons/bi'
|
import { BiRename } from 'react-icons/bi'
|
||||||
import RenameForm from './RenameForm';
|
import RenameForm from './RenameForm';
|
||||||
import ChangeDestination from './ChangeDestination';
|
import ChangeDestination from './ChangeDestination';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||||
import { MdDoubleArrow } from "react-icons/md";
|
import { MdDoubleArrow } from "react-icons/md";
|
||||||
|
|
||||||
export default function ServiceRow({ service }:{ service:Service }) {
|
export default function ServiceRow({ service }:{ service:Service }) {
|
||||||
|
|
||||||
let status_color = service.active ? "teal": "red"
|
let status_color = service.active ? "teal": "red"
|
||||||
|
|
||||||
const [buttonLoading, setButtonLoading] = useState(false)
|
const [buttonLoading, setButtonLoading] = useState(false)
|
||||||
const [deleteModal, setDeleteModal] = useState(false)
|
const [deleteModal, setDeleteModal] = useState(false)
|
||||||
const [renameModal, setRenameModal] = useState(false)
|
const [renameModal, setRenameModal] = useState(false)
|
||||||
const [changeDestModal, setChangeDestModal] = useState(false)
|
const [changeDestModal, setChangeDestModal] = useState(false)
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: { proxy_port:service.proxy_port },
|
initialValues: { proxy_port:service.proxy_port },
|
||||||
validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" }
|
validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" }
|
||||||
})
|
})
|
||||||
|
|
||||||
const stopService = async () => {
|
const stopService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
|
|
||||||
await porthijack.servicestop(service.service_id).then(res => {
|
await porthijack.servicestop(service.service_id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.public_port} has been stopped!`)
|
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.public_port} has been stopped!`)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false);
|
setButtonLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const startService = async () => {
|
const startService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
await porthijack.servicestart(service.service_id).then(res => {
|
await porthijack.servicestart(service.service_id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.public_port} has been started!`)
|
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.public_port} has been started!`)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false)
|
setButtonLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteService = () => {
|
const deleteService = () => {
|
||||||
porthijack.servicedelete(service.service_id).then(res => {
|
porthijack.servicedelete(service.service_id).then(res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||||
}else
|
}else
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Box className='firegex__nfregex__rowbox'>
|
<Box className='firegex__nfregex__rowbox'>
|
||||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||||
<Box>
|
<Box>
|
||||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||||
<Title className="firegex__nfregex__name" ml="xs">
|
<Title className="firegex__nfregex__name" ml="xs">
|
||||||
{service.name}
|
{service.name}
|
||||||
</Title>
|
</Title>
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||||
<Badge color={status_color} radius="md" size="md" variant="filled">{service.active?"ENABLED":"DISABLED"}</Badge>
|
<Badge color={status_color} radius="md" size="md" variant="filled">{service.active?"ENABLED":"DISABLED"}</Badge>
|
||||||
<Badge color={service.proto === "tcp"?"cyan":"orange"} radius="md" size="md" variant="filled">
|
<Badge color={service.proto === "tcp"?"cyan":"orange"} radius="md" size="md" variant="filled">
|
||||||
{service.proto}
|
{service.proto}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space w="xl" />}
|
{isMedium?null:<Space w="xl" />}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||||
<Box className="center-flex-row">
|
<Box className="center-flex-row">
|
||||||
<Badge color="lime" radius="sm" size="lg" variant="filled">
|
<Badge color="lime" radius="sm" size="lg" variant="filled">
|
||||||
FROM {service.ip_src} :{service.public_port}
|
FROM {service.ip_src} :{service.public_port}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Space h="sm" />
|
<Space h="sm" />
|
||||||
<Badge color="blue" radius="sm" size="lg" variant="filled">
|
<Badge color="blue" radius="sm" size="lg" variant="filled">
|
||||||
<Box className="center-flex">
|
<Box className="center-flex">
|
||||||
TO {service.ip_dst} :{service.proxy_port}
|
TO {service.ip_dst} :{service.proxy_port}
|
||||||
</Box>
|
</Box>
|
||||||
</Badge>
|
</Badge>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||||
<Box className="center-flex">
|
<Box className="center-flex">
|
||||||
<MenuDropDownWithButton>
|
<MenuDropDownWithButton>
|
||||||
<Menu.Label><b>Rename service</b></Menu.Label>
|
<Menu.Label><b>Rename service</b></Menu.Label>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||||
<Menu.Label><b>Change destination</b></Menu.Label>
|
<Menu.Label><b>Change destination</b></Menu.Label>
|
||||||
<Menu.Item leftSection={<BsArrowRepeat size={18} />} onClick={()=>setChangeDestModal(true)}>Change hijacking destination</Menu.Item>
|
<Menu.Item leftSection={<BsArrowRepeat size={18} />} onClick={()=>setChangeDestModal(true)}>Change hijacking destination</Menu.Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
</MenuDropDownWithButton>
|
</MenuDropDownWithButton>
|
||||||
<Space w="md"/>
|
<Space w="md"/>
|
||||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||||
<ActionIcon color="red" loading={buttonLoading}
|
<ActionIcon color="red" loading={buttonLoading}
|
||||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||||
disabled={!service.active}
|
disabled={!service.active}
|
||||||
aria-describedby="tooltip-stop-id">
|
aria-describedby="tooltip-stop-id">
|
||||||
<FaStop size="20px" />
|
<FaStop size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="md"/>
|
<Space w="md"/>
|
||||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||||
variant="filled" disabled={service.active}>
|
variant="filled" disabled={service.active}>
|
||||||
<FaPlay size="20px" />
|
<FaPlay size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
title='Are you sure to delete this service?'
|
title='Are you sure to delete this service?'
|
||||||
description={`You are going to delete the service '${service.public_port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
description={`You are going to delete the service '${service.public_port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||||
onClose={()=>setDeleteModal(false) }
|
onClose={()=>setDeleteModal(false) }
|
||||||
action={deleteService}
|
action={deleteService}
|
||||||
opened={deleteModal}
|
opened={deleteModal}
|
||||||
/>
|
/>
|
||||||
<RenameForm
|
<RenameForm
|
||||||
onClose={()=>setRenameModal(false)}
|
onClose={()=>setRenameModal(false)}
|
||||||
opened={renameModal}
|
opened={renameModal}
|
||||||
service={service}
|
service={service}
|
||||||
/>
|
/>
|
||||||
<ChangeDestination
|
<ChangeDestination
|
||||||
onClose={()=>setChangeDestModal(false)}
|
onClose={()=>setChangeDestModal(false)}
|
||||||
opened={changeDestModal}
|
opened={changeDestModal}
|
||||||
service={service}
|
service={service}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,64 @@
|
|||||||
import { ServerResponse } from "../../js/models"
|
import { ServerResponse } from "../../js/models"
|
||||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
|
||||||
export type GeneralStats = {
|
export type GeneralStats = {
|
||||||
services:number
|
services:number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Service = {
|
export type Service = {
|
||||||
name:string,
|
name:string,
|
||||||
service_id:string,
|
service_id:string,
|
||||||
active:boolean,
|
active:boolean,
|
||||||
proto: string,
|
proto: string,
|
||||||
ip_src: string,
|
ip_src: string,
|
||||||
ip_dst: string,
|
ip_dst: string,
|
||||||
proxy_port: number,
|
proxy_port: number,
|
||||||
public_port: number,
|
public_port: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAddForm = {
|
export type ServiceAddForm = {
|
||||||
name:string,
|
name:string,
|
||||||
public_port:number,
|
public_port:number,
|
||||||
proxy_port:number,
|
proxy_port:number,
|
||||||
proto:string,
|
proto:string,
|
||||||
ip_src: string,
|
ip_src: string,
|
||||||
ip_dst: string,
|
ip_dst: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceAddResponse = ServerResponse & { service_id: string }
|
export type ServiceAddResponse = ServerResponse & { service_id: string }
|
||||||
|
|
||||||
export const queryKey = ["porthijack","services"]
|
export const queryKey = ["porthijack","services"]
|
||||||
|
|
||||||
export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services})
|
export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services})
|
||||||
|
|
||||||
export const porthijack = {
|
export const porthijack = {
|
||||||
services: async () : Promise<Service[]> => {
|
services: async () : Promise<Service[]> => {
|
||||||
return await getapi("porthijack/services") as Service[];
|
return await getapi("porthijack/services") as Service[];
|
||||||
},
|
},
|
||||||
serviceinfo: async (service_id:string) => {
|
serviceinfo: async (service_id:string) => {
|
||||||
return await getapi(`porthijack/services/${service_id}`) as Service;
|
return await getapi(`porthijack/services/${service_id}`) as Service;
|
||||||
},
|
},
|
||||||
servicestart: async (service_id:string) => {
|
servicestart: async (service_id:string) => {
|
||||||
const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse;
|
const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicerename: async (service_id:string, name: string) => {
|
servicerename: async (service_id:string, name: string) => {
|
||||||
const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse;
|
const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicestop: async (service_id:string) => {
|
servicestop: async (service_id:string) => {
|
||||||
const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse;
|
const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
servicesadd: async (data:ServiceAddForm) => {
|
servicesadd: async (data:ServiceAddForm) => {
|
||||||
return await postapi("porthijack/services",data) as ServiceAddResponse;
|
return await postapi("porthijack/services",data) as ServiceAddResponse;
|
||||||
},
|
},
|
||||||
servicedelete: async (service_id:string) => {
|
servicedelete: async (service_id:string) => {
|
||||||
const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse;
|
const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse;
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
},
|
},
|
||||||
changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => {
|
changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => {
|
||||||
return await putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse;
|
return await putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,44 +1,44 @@
|
|||||||
import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { PyFilter } from '../../js/models';
|
import { PyFilter } from '../../js/models';
|
||||||
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||||
import { FaPause, FaPlay } from 'react-icons/fa';
|
import { FaPause, FaPlay } from 'react-icons/fa';
|
||||||
import { FaFilter } from "react-icons/fa";
|
import { FaFilter } from "react-icons/fa";
|
||||||
import { nfproxy } from '../NFProxy/utils';
|
import { nfproxy } from '../NFProxy/utils';
|
||||||
import { FaPencilAlt } from 'react-icons/fa';
|
import { FaPencilAlt } from 'react-icons/fa';
|
||||||
|
|
||||||
export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) {
|
export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) {
|
||||||
|
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
|
|
||||||
const changeRegexStatus = () => {
|
const changeRegexStatus = () => {
|
||||||
(filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => {
|
(filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`)
|
okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Box my="sm" display="flex" style={{alignItems:"center"}}>
|
return <Box my="sm" display="flex" style={{alignItems:"center"}}>
|
||||||
|
|
||||||
<Box className="firegex__regexview__pyfilter_text" style={{ width: "100%", alignItems: "center"}} display="flex" >
|
<Box className="firegex__regexview__pyfilter_text" style={{ width: "100%", alignItems: "center"}} display="flex" >
|
||||||
<Badge size="sm" radius="lg" mr="sm" color={filterInfo.active?"lime":"red"} variant="filled" />
|
<Badge size="sm" radius="lg" mr="sm" color={filterInfo.active?"lime":"red"} variant="filled" />
|
||||||
{filterInfo.name}
|
{filterInfo.name}
|
||||||
<Box className='flex-spacer' />
|
<Box className='flex-spacer' />
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
{isMedium?<>
|
{isMedium?<>
|
||||||
<Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge>
|
<Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge>
|
<Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge>
|
||||||
<Space w="lg" />
|
<Space w="lg" />
|
||||||
</>:null}
|
</>:null}
|
||||||
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"}>
|
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"}>
|
||||||
<ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="lg" radius="md" variant="filled">
|
<ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="lg" radius="md" variant="filled">
|
||||||
{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +1,85 @@
|
|||||||
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { RegexFilter } from '../../js/models';
|
import { RegexFilter } from '../../js/models';
|
||||||
import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||||
import { BsTrashFill } from "react-icons/bs"
|
import { BsTrashFill } from "react-icons/bs"
|
||||||
import YesNoModal from '../YesNoModal';
|
import YesNoModal from '../YesNoModal';
|
||||||
import { FaPause, FaPlay } from 'react-icons/fa';
|
import { FaPause, FaPlay } from 'react-icons/fa';
|
||||||
import { useClipboard } from '@mantine/hooks';
|
import { useClipboard } from '@mantine/hooks';
|
||||||
import { FaFilter } from "react-icons/fa";
|
import { FaFilter } from "react-icons/fa";
|
||||||
|
|
||||||
import { nfregex } from '../NFRegex/utils';
|
import { nfregex } from '../NFRegex/utils';
|
||||||
|
|
||||||
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
|
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
|
||||||
|
|
||||||
const mode_string = regexInfo.mode === "C"? "C -> S":
|
const mode_string = regexInfo.mode === "C"? "C -> S":
|
||||||
regexInfo.mode === "S"? "S -> C":
|
regexInfo.mode === "S"? "S -> C":
|
||||||
regexInfo.mode === "B"? "C <-> S": "🤔"
|
regexInfo.mode === "B"? "C <-> S": "🤔"
|
||||||
|
|
||||||
let regex_expr = b64decode(regexInfo.regex);
|
let regex_expr = b64decode(regexInfo.regex);
|
||||||
|
|
||||||
const [deleteModal, setDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
const clipboard = useClipboard({ timeout: 500 });
|
const clipboard = useClipboard({ timeout: 500 });
|
||||||
|
|
||||||
const deleteRegex = () => {
|
const deleteRegex = () => {
|
||||||
nfregex.regexdelete(regexInfo.id).then(res => {
|
nfregex.regexdelete(regexInfo.id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`)
|
okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${res}`)
|
errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch( err => errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${err}`))
|
}).catch( err => errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${err}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeRegexStatus = () => {
|
const changeRegexStatus = () => {
|
||||||
(regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => {
|
(regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`)
|
okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
}).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Box className="firegex__regexview__box">
|
return <Box className="firegex__regexview__box">
|
||||||
<Box>
|
<Box>
|
||||||
<Box className='center-flex' style={{width: "100%"}}>
|
<Box className='center-flex' style={{width: "100%"}}>
|
||||||
<Box className="firegex__regexview__outer_regex_text">
|
<Box className="firegex__regexview__outer_regex_text">
|
||||||
<Text className="firegex__regexview__regex_text" onClick={()=>{
|
<Text className="firegex__regexview__regex_text" onClick={()=>{
|
||||||
clipboard.copy(regex_expr)
|
clipboard.copy(regex_expr)
|
||||||
okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`)
|
okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`)
|
||||||
}}>{regex_expr}</Text>
|
}}>{regex_expr}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"}>
|
<Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"}>
|
||||||
<ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
|
<ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
|
||||||
>{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
>{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label="Delete regex" zIndex={0} color="red" >
|
<Tooltip label="Delete regex" zIndex={0} color="red" >
|
||||||
<ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled">
|
<ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled">
|
||||||
<BsTrashFill size={22} /></ActionIcon>
|
<BsTrashFill size={22} /></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
<Box display="flex" mt="sm" ml="xs">
|
<Box display="flex" mt="sm" ml="xs">
|
||||||
<Badge size="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {regexInfo.n_packets}</Badge>
|
<Badge size="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {regexInfo.n_packets}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" color={regexInfo.active?"lime":"red"} variant="filled">{regexInfo.active?"ACTIVE":"DISABLED"}</Badge>
|
<Badge size="md" color={regexInfo.active?"lime":"red"} variant="filled">{regexInfo.active?"ACTIVE":"DISABLED"}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" color={regexInfo.is_case_sensitive?"grape":"pink"} variant="filled">{regexInfo.is_case_sensitive?"Strict":"Loose"}</Badge>
|
<Badge size="md" color={regexInfo.is_case_sensitive?"grape":"pink"} variant="filled">{regexInfo.is_case_sensitive?"Strict":"Loose"}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" color="blue" variant="filled">{mode_string}</Badge>
|
<Badge size="md" color="blue" variant="filled">{mode_string}</Badge>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
title='Are you sure to delete this regex?'
|
title='Are you sure to delete this regex?'
|
||||||
description={`You are going to delete the regex '${regex_expr}'.`}
|
description={`You are going to delete the regex '${regex_expr}'.`}
|
||||||
onClose={()=>setDeleteModal(false)}
|
onClose={()=>setDeleteModal(false)}
|
||||||
action={deleteRegex}
|
action={deleteRegex}
|
||||||
opened={deleteModal}
|
opened={deleteModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RegexView;
|
export default RegexView;
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { Button, Group, Modal } from '@mantine/core';
|
import { Button, Group, Modal } from '@mantine/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
function YesNoModal( { title, description, action, onClose, opened}:{ title:string, description:string, onClose:()=>void, action:()=>void, opened:boolean} ){
|
function YesNoModal( { title, description, action, onClose, opened}:{ title:string, description:string, onClose:()=>void, action:()=>void, opened:boolean} ){
|
||||||
|
|
||||||
return <Modal size="xl" title={title} opened={opened} onClose={onClose} centered>
|
return <Modal size="xl" title={title} opened={opened} onClose={onClose} centered>
|
||||||
{description}
|
{description}
|
||||||
<Group justify='flex-end' mt="md">
|
<Group justify='flex-end' mt="md">
|
||||||
<Button onClick={()=>{
|
<Button onClick={()=>{
|
||||||
onClose()
|
onClose()
|
||||||
action()
|
action()
|
||||||
}} color="teal" type="submit">Yes</Button>
|
}} color="teal" type="submit">Yes</Button>
|
||||||
<Button onClick={onClose} color="red" type="submit">No</Button>
|
<Button onClick={onClose} color="red" type="submit">No</Button>
|
||||||
|
|
||||||
</Group>
|
</Group>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default YesNoModal;
|
export default YesNoModal;
|
||||||
@@ -42,21 +42,6 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Hook personalizzati per un uso più facile nei componenti
|
|
||||||
export const useAuth = () => {
|
|
||||||
const { access_token, setAccessToken, clearAccessToken, getAccessToken } = useAuthStore();
|
|
||||||
|
|
||||||
const isAuthenticated = !!access_token;
|
|
||||||
|
|
||||||
return {
|
|
||||||
access_token,
|
|
||||||
isAuthenticated,
|
|
||||||
setAccessToken,
|
|
||||||
clearAccessToken,
|
|
||||||
getAccessToken,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SessionState {
|
interface SessionState {
|
||||||
home_section: string | null;
|
home_section: string | null;
|
||||||
setHomeSection: (section: string | null) => void;
|
setHomeSection: (section: string | null) => void;
|
||||||
|
|||||||
@@ -1,227 +1,227 @@
|
|||||||
import { showNotification } from "@mantine/notifications";
|
import { showNotification } from "@mantine/notifications";
|
||||||
import { ImCross } from "react-icons/im";
|
import { ImCross } from "react-icons/im";
|
||||||
import { TiTick } from "react-icons/ti"
|
import { TiTick } from "react-icons/ti"
|
||||||
import { Navigate } from "react-router";
|
import { Navigate } from "react-router";
|
||||||
import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models";
|
import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models";
|
||||||
import { Buffer } from "buffer"
|
import { Buffer } from "buffer"
|
||||||
import { QueryClient, useQuery } from "@tanstack/react-query";
|
import { QueryClient, useQuery } from "@tanstack/react-query";
|
||||||
import { useMediaQuery } from "@mantine/hooks";
|
import { useMediaQuery } from "@mantine/hooks";
|
||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
import { useAuthStore, useSessionStore } from "./store";
|
import { useAuthStore, useSessionStore } from "./store";
|
||||||
|
|
||||||
export const IS_DEV = import.meta.env.DEV
|
export const IS_DEV = import.meta.env.DEV
|
||||||
|
|
||||||
export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$";
|
export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$";
|
||||||
export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$";
|
export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$";
|
||||||
export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$"
|
export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$"
|
||||||
export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
|
export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
|
||||||
export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$"
|
export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$"
|
||||||
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
|
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
|
||||||
export const DEV_IP_BACKEND = "127.0.0.1:4444"
|
export const DEV_IP_BACKEND = "127.0.0.1:4444"
|
||||||
|
|
||||||
export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes
|
export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes
|
||||||
|
|
||||||
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
|
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
|
||||||
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
|
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
|
||||||
|
|
||||||
export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
|
export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
|
||||||
return value as E;
|
return value as E;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const socketio = import.meta.env.DEV?
|
export const socketio = import.meta.env.DEV?
|
||||||
io("ws://"+DEV_IP_BACKEND, {
|
io("ws://"+DEV_IP_BACKEND, {
|
||||||
path:"/sock/socket.io",
|
path:"/sock/socket.io",
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
auth: {
|
auth: {
|
||||||
token: useAuthStore.getState().getAccessToken()
|
token: useAuthStore.getState().getAccessToken()
|
||||||
}
|
}
|
||||||
}):
|
}):
|
||||||
io({
|
io({
|
||||||
path:"/sock/socket.io",
|
path:"/sock/socket.io",
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
auth: {
|
auth: {
|
||||||
token: useAuthStore.getState().getAccessToken()
|
token: useAuthStore.getState().getAccessToken()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const queryClient = new QueryClient({ defaultOptions: { queries: {
|
export const queryClient = new QueryClient({ defaultOptions: { queries: {
|
||||||
staleTime: Infinity
|
staleTime: Infinity
|
||||||
} }})
|
} }})
|
||||||
|
|
||||||
export function getErrorMessage(e: any) {
|
export function getErrorMessage(e: any) {
|
||||||
let error = "Unknown error";
|
let error = "Unknown error";
|
||||||
if(typeof e == "string") return e
|
if(typeof e == "string") return e
|
||||||
if (e.response) {
|
if (e.response) {
|
||||||
// The request was made and the server responded with a status code
|
// The request was made and the server responded with a status code
|
||||||
// that falls out of the range of 2xx
|
// that falls out of the range of 2xx
|
||||||
error = e.response.data.error;
|
error = e.response.data.error;
|
||||||
} else {
|
} else {
|
||||||
// Something happened in setting up the request that triggered an Error
|
// Something happened in setting up the request that triggered an Error
|
||||||
error = e.message || e.error;
|
error = e.message || e.error;
|
||||||
}
|
}
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") {
|
export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") {
|
||||||
if (e.status){
|
if (e.status){
|
||||||
return e.status
|
return e.status
|
||||||
}
|
}
|
||||||
if (e.detail){
|
if (e.detail){
|
||||||
if (typeof e.detail == "string")
|
if (typeof e.detail == "string")
|
||||||
return e.detail
|
return e.detail
|
||||||
if (e.detail[0] && e.detail[0].msg)
|
if (e.detail[0] && e.detail[0].msg)
|
||||||
return e.detail[0].msg
|
return e.detail[0].msg
|
||||||
}
|
}
|
||||||
if (e.error){
|
if (e.error){
|
||||||
return e.error
|
return e.error
|
||||||
}
|
}
|
||||||
return def
|
return def
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
|
export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
|
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
|
||||||
method: method,
|
method: method,
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
cache: 'no-cache',
|
cache: 'no-cache',
|
||||||
headers: {
|
headers: {
|
||||||
...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}),
|
...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}),
|
||||||
"Authorization" : "Bearer " + useAuthStore.getState().getAccessToken()
|
"Authorization" : "Bearer " + useAuthStore.getState().getAccessToken()
|
||||||
},
|
},
|
||||||
body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined
|
body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
if(res.status === 401) window.location.reload()
|
if(res.status === 401) window.location.reload()
|
||||||
if(res.status === 406) resolve({status:"Wrong Password"})
|
if(res.status === 406) resolve({status:"Wrong Password"})
|
||||||
if(!res.ok){
|
if(!res.ok){
|
||||||
const errorDefault = res.statusText
|
const errorDefault = res.statusText
|
||||||
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
|
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
|
||||||
}
|
}
|
||||||
res.text().then(t => {
|
res.text().then(t => {
|
||||||
try{
|
try{
|
||||||
resolve(JSON.parse(t))
|
resolve(JSON.parse(t))
|
||||||
}catch(e){
|
}catch(e){
|
||||||
resolve(t)
|
resolve(t)
|
||||||
}
|
}
|
||||||
}).catch( err => reject(err))
|
}).catch( err => reject(err))
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
reject(err)
|
reject(err)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getapi(path:string):Promise<any>{
|
export async function getapi(path:string):Promise<any>{
|
||||||
return await genericapi("GET",path)
|
return await genericapi("GET",path)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise<any>{
|
export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise<any>{
|
||||||
return await genericapi("POST",path,data,is_form)
|
return await genericapi("POST",path,data,is_form)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteapi(path:string):Promise<any>{
|
export async function deleteapi(path:string):Promise<any>{
|
||||||
return await genericapi("DELETE",path)
|
return await genericapi("DELETE",path)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function putapi(path:string,data:any):Promise<any>{
|
export async function putapi(path:string,data:any):Promise<any>{
|
||||||
return await genericapi("PUT",path,data)
|
return await genericapi("PUT",path,data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMainPath(){
|
export function getMainPath(){
|
||||||
const paths = window.location.pathname.split("/")
|
const paths = window.location.pathname.split("/")
|
||||||
if (paths.length > 1) return paths[1]
|
if (paths.length > 1) return paths[1]
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HomeRedirector(){
|
export function HomeRedirector(){
|
||||||
const section = useSessionStore.getState().getHomeSection();
|
const section = useSessionStore.getState().getHomeSection();
|
||||||
const path = section?`/${section}`:`/nfregex`
|
const path = section?`/${section}`:`/nfregex`
|
||||||
return <Navigate to={path} replace/>
|
return <Navigate to={path} replace/>
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetfiregex(delete_data:boolean = false){
|
export async function resetfiregex(delete_data:boolean = false){
|
||||||
const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse;
|
const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse;
|
||||||
return (status === "ok"?undefined:status)
|
return (status === "ok"?undefined:status)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces)
|
export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces)
|
||||||
|
|
||||||
export async function getipinterfaces(){
|
export async function getipinterfaces(){
|
||||||
return await getapi("interfaces") as IpInterface[];
|
return await getapi("interfaces") as IpInterface[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getstatus(){
|
export async function getstatus(){
|
||||||
return await getapi(`status`) as ServerStatusResponse;
|
return await getapi(`status`) as ServerStatusResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout(){
|
export async function logout(){
|
||||||
useAuthStore.getState().clearAccessToken();
|
useAuthStore.getState().clearAccessToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setpassword(data:PasswordSend) {
|
export async function setpassword(data:PasswordSend) {
|
||||||
const { status, access_token } = await postapi("set-password",data) as ServerResponseToken;
|
const { status, access_token } = await postapi("set-password",data) as ServerResponseToken;
|
||||||
if (access_token)
|
if (access_token)
|
||||||
useAuthStore.getState().setAccessToken(access_token);
|
useAuthStore.getState().setAccessToken(access_token);
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function changepassword(data:ChangePassword) {
|
export async function changepassword(data:ChangePassword) {
|
||||||
const { status, access_token } = await postapi("change-password",data) as ServerResponseToken;
|
const { status, access_token } = await postapi("change-password",data) as ServerResponseToken;
|
||||||
if (access_token)
|
if (access_token)
|
||||||
useAuthStore.getState().setAccessToken(access_token);
|
useAuthStore.getState().setAccessToken(access_token);
|
||||||
return status === "ok"?undefined:status
|
return status === "ok"?undefined:status
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(data:PasswordSend) {
|
export async function login(data:PasswordSend) {
|
||||||
const from = {username: "login", password: data.password};
|
const from = {username: "login", password: data.password};
|
||||||
const { status, access_token } = await postapi("login",from,true) as LoginResponse;
|
const { status, access_token } = await postapi("login",from,true) as LoginResponse;
|
||||||
useAuthStore.getState().setAccessToken(access_token);
|
useAuthStore.getState().setAccessToken(access_token);
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function errorNotify(title:string, description:string ){
|
export function errorNotify(title:string, description:string ){
|
||||||
showNotification({
|
showNotification({
|
||||||
autoClose: 2000,
|
autoClose: 2000,
|
||||||
title: title,
|
title: title,
|
||||||
message: description,
|
message: description,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <ImCross />,
|
icon: <ImCross />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function okNotify(title:string, description:string ){
|
export function okNotify(title:string, description:string ){
|
||||||
showNotification({
|
showNotification({
|
||||||
autoClose: 2000,
|
autoClose: 2000,
|
||||||
title: title,
|
title: title,
|
||||||
message: description,
|
message: description,
|
||||||
color: 'teal',
|
color: 'teal',
|
||||||
icon: <TiTick />,
|
icon: <TiTick />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeid = (length:number) => {
|
export const makeid = (length:number) => {
|
||||||
let result = '';
|
let result = '';
|
||||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
const charactersLength = characters.length;
|
const charactersLength = characters.length;
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
while (counter < length) {
|
while (counter < length) {
|
||||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||||
counter += 1;
|
counter += 1;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function b64encode(data:number[]|string){
|
export function b64encode(data:number[]|string){
|
||||||
return Buffer.from(data).toString('base64')
|
return Buffer.from(data).toString('base64')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function b64decode(regexB64:string){
|
export function b64decode(regexB64:string){
|
||||||
return Buffer.from(regexB64, "base64").toString()
|
return Buffer.from(regexB64, "base64").toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMediumScreen(){
|
export function isMediumScreen(){
|
||||||
return useMediaQuery('(min-width: 600px)');
|
return useMediaQuery('(min-width: 600px)');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isLargeScreen(){
|
export function isLargeScreen(){
|
||||||
return useMediaQuery('(min-width: 992px)');
|
return useMediaQuery('(min-width: 992px)');
|
||||||
}
|
}
|
||||||
@@ -1,237 +1,243 @@
|
|||||||
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||||
import { Badge, Divider, Menu } from '@mantine/core';
|
import { Badge, Divider, Menu } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
|
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
|
||||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
|
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
|
||||||
import { MdDoubleArrow } from "react-icons/md"
|
import { MdDoubleArrow } from "react-icons/md"
|
||||||
import YesNoModal from '../../components/YesNoModal';
|
import YesNoModal from '../../components/YesNoModal';
|
||||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
|
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
|
||||||
import { BsTrashFill } from 'react-icons/bs';
|
import { BsTrashFill } from 'react-icons/bs';
|
||||||
import { BiRename } from 'react-icons/bi'
|
import { BiRename } from 'react-icons/bi'
|
||||||
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
|
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
|
||||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { FaArrowLeft } from "react-icons/fa";
|
import { FaArrowLeft } from "react-icons/fa";
|
||||||
import { IoSettingsSharp } from 'react-icons/io5';
|
import { IoSettingsSharp } from 'react-icons/io5';
|
||||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||||
import PyFilterView from '../../components/PyFilterView';
|
import PyFilterView from '../../components/PyFilterView';
|
||||||
import { TbPlugConnected } from 'react-icons/tb';
|
import { TbPlugConnected } from 'react-icons/tb';
|
||||||
import { CodeHighlight } from '@mantine/code-highlight';
|
import { CodeHighlight } from '@mantine/code-highlight';
|
||||||
import { FaPython } from "react-icons/fa";
|
import { FaPython } from "react-icons/fa";
|
||||||
import { FiFileText } from "react-icons/fi";
|
import { FiFileText } from "react-icons/fi";
|
||||||
import { ModalLog } from '../../components/ModalLog';
|
import { ModalLog } from '../../components/ModalLog';
|
||||||
import { useListState } from '@mantine/hooks';
|
import { useListState } from '@mantine/hooks';
|
||||||
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
||||||
import { DocsButton } from '../../components/DocsButton';
|
import { DocsButton } from '../../components/DocsButton';
|
||||||
|
|
||||||
export default function ServiceDetailsNFProxy() {
|
export default function ServiceDetailsNFProxy() {
|
||||||
|
|
||||||
const {srv} = useParams()
|
const {srv} = useParams()
|
||||||
const services = nfproxyServiceQuery()
|
const services = nfproxyServiceQuery()
|
||||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||||
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
|
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
|
||||||
const [deleteModal, setDeleteModal] = useState(false)
|
const [deleteModal, setDeleteModal] = useState(false)
|
||||||
const [renameModal, setRenameModal] = useState(false)
|
const [renameModal, setRenameModal] = useState(false)
|
||||||
const [editModal, setEditModal] = useState(false)
|
const [editModal, setEditModal] = useState(false)
|
||||||
const [buttonLoading, setButtonLoading] = useState(false)
|
const [buttonLoading, setButtonLoading] = useState(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
|
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
const [openLogModal, setOpenLogModal] = useState(false)
|
const [openLogModal, setOpenLogModal] = useState(false)
|
||||||
const [logData, logDataSetters] = useListState<string>([]);
|
const [logData, logDataSetters] = useListState<string>([]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if (srv){
|
if (srv){
|
||||||
if (openLogModal){
|
if (openLogModal){
|
||||||
logDataSetters.setState([])
|
logDataSetters.setState([])
|
||||||
socketio.emit("nfproxy-outstream-join", { service: srv });
|
socketio.emit("nfproxy-outstream-join", { service: srv });
|
||||||
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
|
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
|
||||||
logDataSetters.append(data)
|
logDataSetters.append(data)
|
||||||
});
|
});
|
||||||
}else{
|
}else{
|
||||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||||
socketio.off(`nfproxy-outstream-${srv}`);
|
socketio.off(`nfproxy-outstream-${srv}`);
|
||||||
logDataSetters.setState([])
|
logDataSetters.setState([])
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||||
socketio.off(`nfproxy-outstream-${srv}`);
|
socketio.off(`nfproxy-outstream-${srv}`);
|
||||||
logDataSetters.setState([])
|
logDataSetters.setState([])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [openLogModal, srv])
|
}, [openLogModal, srv])
|
||||||
|
|
||||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||||
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
|
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
|
||||||
|
|
||||||
let status_color = "gray";
|
let status_color = "gray";
|
||||||
switch(serviceInfo.status){
|
switch(serviceInfo.status){
|
||||||
case "stop": status_color = "red"; break;
|
case "stop": status_color = "red"; break;
|
||||||
case "active": status_color = "teal"; break;
|
case "active": status_color = "teal"; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startService = async () => {
|
const startService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
|
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false)
|
setButtonLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteService = () => {
|
const deleteService = () => {
|
||||||
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
|
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else
|
}else
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopService = async () => {
|
const stopService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
|
|
||||||
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
|
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false);
|
setButtonLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<LoadingOverlay visible={filtersList.isLoading} />
|
<LoadingOverlay visible={filtersList.isLoading} />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||||
<Box>
|
<Box>
|
||||||
<Title order={1}>
|
<Title order={1}>
|
||||||
<Box className="center-flex">
|
<Box className="center-flex">
|
||||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||||
</Box>
|
</Box>
|
||||||
</Title>
|
</Title>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space h="md" />}
|
{isMedium?null:<Space h="md" />}
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<ExceptionWarning service_id={srv} />
|
<ExceptionWarning service_id={srv} />
|
||||||
<Space w="sm" />
|
<Space w="sm" />
|
||||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||||
{serviceInfo.status}
|
{serviceInfo.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||||
:{serviceInfo.port}
|
:{serviceInfo.port}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<MenuDropDownWithButton>
|
<MenuDropDownWithButton>
|
||||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
</MenuDropDownWithButton>
|
</MenuDropDownWithButton>
|
||||||
<Space w="md"/>
|
<Space w="md"/>
|
||||||
<Tooltip label="Show logs" zIndex={0} color="cyan">
|
<Tooltip label="Show logs" zIndex={0} color="cyan">
|
||||||
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
|
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
|
||||||
<FiFileText size="20px" />
|
<FiFileText size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
<Space w="xs"/>
|
||||||
</Box>
|
<Tooltip label="Traffic viewer" zIndex={0} color="grape">
|
||||||
{isMedium?null:<Space h="md" />}
|
<ActionIcon color="grape" size="lg" radius="md" onClick={()=>navigate(`/nfproxy/${srv}/traffic`)} variant="filled">
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
<MdDoubleArrow size="20px" />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
</ActionIcon>
|
||||||
<Box className='center-flex'>
|
</Tooltip>
|
||||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
|
</Box>
|
||||||
<Space w="xs" />
|
</Box>
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
|
{isMedium?null:<Space h="md" />}
|
||||||
<Space w="xs" />
|
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
|
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||||
</Box>
|
<Box className='center-flex'>
|
||||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
|
||||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
<Space w="xs" />
|
||||||
</Box>
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
|
||||||
{isMedium?null:<Space h="xl" />}
|
<Space w="xs" />
|
||||||
<Box className='center-flex'>
|
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
|
||||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
</Box>
|
||||||
<ActionIcon color="cyan"
|
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||||
aria-describedby="tooltip-back-id">
|
</Box>
|
||||||
<FaArrowLeft size="25px" />
|
{isMedium?null:<Space h="xl" />}
|
||||||
</ActionIcon>
|
<Box className='center-flex'>
|
||||||
</Tooltip>
|
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||||
<Space w="md"/>
|
<ActionIcon color="cyan"
|
||||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||||
<ActionIcon color="red" loading={buttonLoading}
|
aria-describedby="tooltip-back-id">
|
||||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
<FaArrowLeft size="25px" />
|
||||||
disabled={serviceInfo.status === "stop"}
|
</ActionIcon>
|
||||||
aria-describedby="tooltip-stop-id">
|
</Tooltip>
|
||||||
<FaStop size="20px" />
|
<Space w="md"/>
|
||||||
</ActionIcon>
|
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||||
</Tooltip>
|
<ActionIcon color="red" loading={buttonLoading}
|
||||||
<Space w="md"/>
|
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
disabled={serviceInfo.status === "stop"}
|
||||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
aria-describedby="tooltip-stop-id">
|
||||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
<FaStop size="20px" />
|
||||||
<FaPlay size="20px" />
|
</ActionIcon>
|
||||||
</ActionIcon>
|
</Tooltip>
|
||||||
</Tooltip>
|
<Space w="md"/>
|
||||||
</Box>
|
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||||
</Box>
|
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||||
|
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||||
<Divider my="xl" />
|
<FaPlay size="20px" />
|
||||||
|
</ActionIcon>
|
||||||
{filterCode.data?<>
|
</Tooltip>
|
||||||
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
|
</Box>
|
||||||
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
|
</Box>
|
||||||
</>: null}
|
|
||||||
|
<Divider my="xl" />
|
||||||
{(!filtersList.data || filtersList.data.length == 0)?<>
|
|
||||||
<Space h="xl" />
|
{filterCode.data?<>
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
|
||||||
<Space h="xs" />
|
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
|
</>: null}
|
||||||
<Space h="xs" />
|
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
|
{(!filtersList.data || filtersList.data.length == 0)?<>
|
||||||
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
|
<Space h="xl" />
|
||||||
}
|
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||||
<YesNoModal
|
<Space h="xs" />
|
||||||
title='Are you sure to delete this service?'
|
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
|
||||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
<Space h="xs" />
|
||||||
onClose={()=>setDeleteModal(false) }
|
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
|
||||||
action={deleteService}
|
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
|
||||||
opened={deleteModal}
|
}
|
||||||
/>
|
<YesNoModal
|
||||||
<RenameForm
|
title='Are you sure to delete this service?'
|
||||||
onClose={()=>setRenameModal(false)}
|
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||||
opened={renameModal}
|
onClose={()=>setDeleteModal(false) }
|
||||||
service={serviceInfo}
|
action={deleteService}
|
||||||
/>
|
opened={deleteModal}
|
||||||
<AddEditService
|
/>
|
||||||
opened={editModal}
|
<RenameForm
|
||||||
onClose={()=>setEditModal(false)}
|
onClose={()=>setRenameModal(false)}
|
||||||
edit={serviceInfo}
|
opened={renameModal}
|
||||||
/>
|
service={serviceInfo}
|
||||||
<ModalLog
|
/>
|
||||||
opened={openLogModal}
|
<AddEditService
|
||||||
close={()=>setOpenLogModal(false)}
|
opened={editModal}
|
||||||
title={`Logs for service ${serviceInfo.name}`}
|
onClose={()=>setEditModal(false)}
|
||||||
data={logData.join("")}
|
edit={serviceInfo}
|
||||||
/>
|
/>
|
||||||
</>
|
<ModalLog
|
||||||
}
|
opened={openLogModal}
|
||||||
|
close={()=>setOpenLogModal(false)}
|
||||||
|
title={`Logs for service ${serviceInfo.name}`}
|
||||||
|
data={logData.join("")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|||||||
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 { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { BsPlusLg } from "react-icons/bs";
|
import { BsPlusLg } from "react-icons/bs";
|
||||||
import { useNavigate, useParams } from 'react-router';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
import ServiceRow from '../../components/NFProxy/ServiceRow';
|
import ServiceRow from '../../components/NFProxy/ServiceRow';
|
||||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { TbPlugConnected, TbReload } from 'react-icons/tb';
|
import { TbPlugConnected, TbReload } from 'react-icons/tb';
|
||||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||||
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
|
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
|
||||||
import { MdUploadFile } from "react-icons/md";
|
import { MdUploadFile } from "react-icons/md";
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
import { useFileDialog } from '@mantine/hooks';
|
import { useFileDialog } from '@mantine/hooks';
|
||||||
import { CodeHighlight } from '@mantine/code-highlight';
|
import { CodeHighlight } from '@mantine/code-highlight';
|
||||||
import { DocsButton } from '../../components/DocsButton';
|
import { DocsButton } from '../../components/DocsButton';
|
||||||
|
|
||||||
|
|
||||||
export default function NFProxy({ children }: { children: any }) {
|
export default function NFProxy({ children }: { children: any }) {
|
||||||
|
|
||||||
const navigator = useNavigate()
|
const navigator = useNavigate()
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const {srv} = useParams()
|
const {srv} = useParams()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
const services = nfproxyServiceQuery()
|
const services = nfproxyServiceQuery()
|
||||||
const fileDialog = useFileDialog({
|
const fileDialog = useFileDialog({
|
||||||
accept: ".py",
|
accept: ".py",
|
||||||
multiple: false,
|
multiple: false,
|
||||||
resetOnOpen: true,
|
resetOnOpen: true,
|
||||||
onChange: (files) => {
|
onChange: (files) => {
|
||||||
if (files?.length??0 > 0)
|
if (files?.length??0 > 0)
|
||||||
setFile(files![0])
|
setFile(files![0])
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!srv) return
|
if (!srv) return
|
||||||
const service = services.data?.find(s => s.service_id === srv)
|
const service = services.data?.find(s => s.service_id === srv)
|
||||||
if (!service) return
|
if (!service) return
|
||||||
if (file){
|
if (file){
|
||||||
console.log("Uploading code")
|
console.log("Uploading code")
|
||||||
const notify_id = notifications.show(
|
const notify_id = notifications.show(
|
||||||
{
|
{
|
||||||
title: "Uploading code",
|
title: "Uploading code",
|
||||||
message: `Uploading code for service ${service.name}`,
|
message: `Uploading code for service ${service.name}`,
|
||||||
color: "blue",
|
color: "blue",
|
||||||
icon: <MdUploadFile size={20} />,
|
icon: <MdUploadFile size={20} />,
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
file.text()
|
file.text()
|
||||||
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
|
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
|
||||||
.then( res => {
|
.then( res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
notifications.update({
|
notifications.update({
|
||||||
id: notify_id,
|
id: notify_id,
|
||||||
title: "Code uploaded",
|
title: "Code uploaded",
|
||||||
message: `Successfully uploaded code for service ${service.name}`,
|
message: `Successfully uploaded code for service ${service.name}`,
|
||||||
color: "green",
|
color: "green",
|
||||||
icon: <MdUploadFile size={20} />,
|
icon: <MdUploadFile size={20} />,
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
loading: false,
|
loading: false,
|
||||||
})
|
})
|
||||||
}else{
|
}else{
|
||||||
notifications.update({
|
notifications.update({
|
||||||
id: notify_id,
|
id: notify_id,
|
||||||
title: "Code upload failed",
|
title: "Code upload failed",
|
||||||
message: `Error: ${res}`,
|
message: `Error: ${res}`,
|
||||||
color: "red",
|
color: "red",
|
||||||
icon: <MdUploadFile size={20} />,
|
icon: <MdUploadFile size={20} />,
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
loading: false,
|
loading: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
notifications.update({
|
notifications.update({
|
||||||
id: notify_id,
|
id: notify_id,
|
||||||
title: "Code upload failed",
|
title: "Code upload failed",
|
||||||
message: `Error: ${err}`,
|
message: `Error: ${err}`,
|
||||||
color: "red",
|
color: "red",
|
||||||
icon: <MdUploadFile size={20} />,
|
icon: <MdUploadFile size={20} />,
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
loading: false,
|
loading: false,
|
||||||
})
|
})
|
||||||
}).finally(()=>{setFile(null)})
|
}).finally(()=>{setFile(null)})
|
||||||
}
|
}
|
||||||
}, [file])
|
}, [file])
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
if(services.isError)
|
if(services.isError)
|
||||||
errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
|
errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
|
||||||
},[services.isError])
|
},[services.isError])
|
||||||
|
|
||||||
const closeModal = () => {setOpen(false);}
|
const closeModal = () => {setOpen(false);}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Space h="sm" />
|
<Space h="sm" />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
|
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
|
||||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||||
<Box className='center-flex' >
|
<Box className='center-flex' >
|
||||||
{isMedium?"General stats:":null}
|
{isMedium?"General stats:":null}
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
|
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
|
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space h="md" />}
|
{isMedium?null:<Space h="md" />}
|
||||||
<Box className='center-flex' >
|
<Box className='center-flex' >
|
||||||
{ srv?
|
{ srv?
|
||||||
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
|
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
|
||||||
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
|
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
|
||||||
<MdUploadFile size={18} />
|
<MdUploadFile size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
|
||||||
<BsPlusLg size={18} />
|
<BsPlusLg size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
|
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
|
||||||
<TbReload size={18} />
|
<TbReload size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<DocsButton doc="nfproxy" />
|
<DocsButton doc="nfproxy" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Box className="center-flex-row" style={{gap: 20}}>
|
<Box className="center-flex-row" style={{gap: 20}}>
|
||||||
{srv?null:<>
|
{srv?null:<>
|
||||||
<LoadingOverlay visible={services.isLoading} />
|
<LoadingOverlay visible={services.isLoading} />
|
||||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||||
navigator("/nfproxy/"+srv.service_id)
|
navigator("/nfproxy/"+srv.service_id)
|
||||||
}} />):<>
|
}} />):<>
|
||||||
<Box className='center-flex-row'>
|
<Box className='center-flex-row'>
|
||||||
<Space h="xl" />
|
<Space h="xl" />
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter proxy is a simulated proxy written using python with a c++ core</Title>
|
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter proxy is a simulated proxy written using python with a c++ core</Title>
|
||||||
<Space h="xs" />
|
<Space h="xs" />
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||||
<Space h="xs" />
|
<Space h="xs" />
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Then you can create a new service and write custom filters for the service</Title>
|
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Then you can create a new service and write custom filters for the service</Title>
|
||||||
<Space h="lg" />
|
<Space h="lg" />
|
||||||
<Box className='center-flex' style={{gap: 20}}>
|
<Box className='center-flex' style={{gap: 20}}>
|
||||||
<Tooltip label="Add a new service" color="blue">
|
<Tooltip label="Add a new service" color="blue">
|
||||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||||
<BsPlusLg size="20px" />
|
<BsPlusLg size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DocsButton doc="nfproxy" size="xl" />
|
<DocsButton doc="nfproxy" size="xl" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>}
|
</>}
|
||||||
</>}
|
</>}
|
||||||
</Box>
|
</Box>
|
||||||
{srv?children:null}
|
{srv?children:null}
|
||||||
{!srv?
|
{!srv?
|
||||||
<AddEditService opened={open} onClose={closeModal} />:null
|
<AddEditService opened={open} onClose={closeModal} />:null
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,194 +1,194 @@
|
|||||||
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||||
import RegexView from '../../components/RegexView';
|
import RegexView from '../../components/RegexView';
|
||||||
import AddNewRegex from '../../components/AddNewRegex';
|
import AddNewRegex from '../../components/AddNewRegex';
|
||||||
import { BsPlusLg } from "react-icons/bs";
|
import { BsPlusLg } from "react-icons/bs";
|
||||||
import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils';
|
import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils';
|
||||||
import { Badge, Divider, Menu } from '@mantine/core';
|
import { Badge, Divider, Menu } from '@mantine/core';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
|
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
|
||||||
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
|
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
|
||||||
import { MdDoubleArrow } from "react-icons/md"
|
import { MdDoubleArrow } from "react-icons/md"
|
||||||
import YesNoModal from '../../components/YesNoModal';
|
import YesNoModal from '../../components/YesNoModal';
|
||||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
|
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
|
||||||
import { BsTrashFill } from 'react-icons/bs';
|
import { BsTrashFill } from 'react-icons/bs';
|
||||||
import { BiRename } from 'react-icons/bi'
|
import { BiRename } from 'react-icons/bi'
|
||||||
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
|
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
|
||||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { FaArrowLeft } from "react-icons/fa";
|
import { FaArrowLeft } from "react-icons/fa";
|
||||||
import { VscRegex } from 'react-icons/vsc';
|
import { VscRegex } from 'react-icons/vsc';
|
||||||
import { IoSettingsSharp } from 'react-icons/io5';
|
import { IoSettingsSharp } from 'react-icons/io5';
|
||||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||||
|
|
||||||
export default function ServiceDetailsNFRegex() {
|
export default function ServiceDetailsNFRegex() {
|
||||||
|
|
||||||
const {srv} = useParams()
|
const {srv} = useParams()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const services = nfregexServiceQuery()
|
const services = nfregexServiceQuery()
|
||||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||||
const regexesList = nfregexServiceRegexesQuery(srv??"")
|
const regexesList = nfregexServiceRegexesQuery(srv??"")
|
||||||
const [deleteModal, setDeleteModal] = useState(false)
|
const [deleteModal, setDeleteModal] = useState(false)
|
||||||
const [renameModal, setRenameModal] = useState(false)
|
const [renameModal, setRenameModal] = useState(false)
|
||||||
const [editModal, setEditModal] = useState(false)
|
const [editModal, setEditModal] = useState(false)
|
||||||
const [buttonLoading, setButtonLoading] = useState(false)
|
const [buttonLoading, setButtonLoading] = useState(false)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
|
|
||||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||||
if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace />
|
if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace />
|
||||||
|
|
||||||
let status_color = "gray";
|
let status_color = "gray";
|
||||||
switch(serviceInfo.status){
|
switch(serviceInfo.status){
|
||||||
case "stop": status_color = "red"; break;
|
case "stop": status_color = "red"; break;
|
||||||
case "active": status_color = "teal"; break;
|
case "active": status_color = "teal"; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startService = async () => {
|
const startService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
await nfregex.servicestart(serviceInfo.service_id).then(res => {
|
await nfregex.servicestart(serviceInfo.service_id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false)
|
setButtonLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteService = () => {
|
const deleteService = () => {
|
||||||
nfregex.servicedelete(serviceInfo.service_id).then(res => {
|
nfregex.servicedelete(serviceInfo.service_id).then(res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else
|
}else
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopService = async () => {
|
const stopService = async () => {
|
||||||
setButtonLoading(true)
|
setButtonLoading(true)
|
||||||
|
|
||||||
await nfregex.servicestop(serviceInfo.service_id).then(res => {
|
await nfregex.servicestop(serviceInfo.service_id).then(res => {
|
||||||
if(!res){
|
if(!res){
|
||||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||||
queryClient.invalidateQueries(serviceQueryKey)
|
queryClient.invalidateQueries(serviceQueryKey)
|
||||||
}else{
|
}else{
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||||
})
|
})
|
||||||
setButtonLoading(false);
|
setButtonLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<LoadingOverlay visible={regexesList.isLoading} />
|
<LoadingOverlay visible={regexesList.isLoading} />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||||
<Box>
|
<Box>
|
||||||
<Title order={1}>
|
<Title order={1}>
|
||||||
<Box className="center-flex">
|
<Box className="center-flex">
|
||||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||||
</Box>
|
</Box>
|
||||||
</Title>
|
</Title>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space h="md" />}
|
{isMedium?null:<Space h="md" />}
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||||
{serviceInfo.status}
|
{serviceInfo.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||||
:{serviceInfo.port}
|
:{serviceInfo.port}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<MenuDropDownWithButton>
|
<MenuDropDownWithButton>
|
||||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||||
</MenuDropDownWithButton>
|
</MenuDropDownWithButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space h="md" />}
|
{isMedium?null:<Space h="md" />}
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
|
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space h="xl" />}
|
{isMedium?null:<Space h="xl" />}
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||||
<ActionIcon color="cyan"
|
<ActionIcon color="cyan"
|
||||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||||
aria-describedby="tooltip-back-id">
|
aria-describedby="tooltip-back-id">
|
||||||
<FaArrowLeft size="25px" />
|
<FaArrowLeft size="25px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="md"/>
|
<Space w="md"/>
|
||||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||||
<ActionIcon color="red" loading={buttonLoading}
|
<ActionIcon color="red" loading={buttonLoading}
|
||||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||||
disabled={serviceInfo.status === "stop"}
|
disabled={serviceInfo.status === "stop"}
|
||||||
aria-describedby="tooltip-stop-id">
|
aria-describedby="tooltip-stop-id">
|
||||||
<FaStop size="20px" />
|
<FaStop size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="md"/>
|
<Space w="md"/>
|
||||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||||
<FaPlay size="20px" />
|
<FaPlay size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Divider my="xl" />
|
<Divider my="xl" />
|
||||||
{(!regexesList.data || regexesList.data.length == 0)?<>
|
{(!regexesList.data || regexesList.data.length == 0)?<>
|
||||||
<Space h="xl" />
|
<Space h="xl" />
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
|
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
|
||||||
<Space h="xl" /> <Space h="xl" />
|
<Space h="xl" /> <Space h="xl" />
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Tooltip label="Add a new regex" zIndex={0} color="blue">
|
<Tooltip label="Add a new regex" zIndex={0} color="blue">
|
||||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
|
||||||
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
|
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
</>:
|
</>:
|
||||||
<Grid>
|
<Grid>
|
||||||
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
|
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
|
||||||
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
|
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
|
||||||
<YesNoModal
|
<YesNoModal
|
||||||
title='Are you sure to delete this service?'
|
title='Are you sure to delete this service?'
|
||||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||||
onClose={()=>setDeleteModal(false) }
|
onClose={()=>setDeleteModal(false) }
|
||||||
action={deleteService}
|
action={deleteService}
|
||||||
opened={deleteModal}
|
opened={deleteModal}
|
||||||
/>
|
/>
|
||||||
<RenameForm
|
<RenameForm
|
||||||
onClose={()=>setRenameModal(false)}
|
onClose={()=>setRenameModal(false)}
|
||||||
opened={renameModal}
|
opened={renameModal}
|
||||||
service={serviceInfo}
|
service={serviceInfo}
|
||||||
/>
|
/>
|
||||||
<AddEditService
|
<AddEditService
|
||||||
opened={editModal}
|
opened={editModal}
|
||||||
onClose={()=>setEditModal(false)}
|
onClose={()=>setEditModal(false)}
|
||||||
edit={serviceInfo}
|
edit={serviceInfo}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +1,100 @@
|
|||||||
import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { BsPlusLg, BsRegex } from "react-icons/bs";
|
import { BsPlusLg, BsRegex } from "react-icons/bs";
|
||||||
import { useNavigate, useParams } from 'react-router';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
import ServiceRow from '../../components/NFRegex/ServiceRow';
|
import ServiceRow from '../../components/NFRegex/ServiceRow';
|
||||||
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
||||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||||
import AddNewRegex from '../../components/AddNewRegex';
|
import AddNewRegex from '../../components/AddNewRegex';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { TbReload } from 'react-icons/tb';
|
import { TbReload } from 'react-icons/tb';
|
||||||
import { FaFilter } from 'react-icons/fa';
|
import { FaFilter } from 'react-icons/fa';
|
||||||
import { FaServer } from "react-icons/fa6";
|
import { FaServer } from "react-icons/fa6";
|
||||||
import { VscRegex } from "react-icons/vsc";
|
import { VscRegex } from "react-icons/vsc";
|
||||||
import { DocsButton } from '../../components/DocsButton';
|
import { DocsButton } from '../../components/DocsButton';
|
||||||
|
|
||||||
function NFRegex({ children }: { children: any }) {
|
function NFRegex({ children }: { children: any }) {
|
||||||
|
|
||||||
const navigator = useNavigate()
|
const navigator = useNavigate()
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const {srv} = useParams()
|
const {srv} = useParams()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
const services = nfregexServiceQuery()
|
const services = nfregexServiceQuery()
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(()=> {
|
||||||
if(services.isError)
|
if(services.isError)
|
||||||
errorNotify("NFRegex Update failed!", getErrorMessage(services.error))
|
errorNotify("NFRegex Update failed!", getErrorMessage(services.error))
|
||||||
},[services.isError])
|
},[services.isError])
|
||||||
|
|
||||||
const closeModal = () => {setOpen(false);}
|
const closeModal = () => {setOpen(false);}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Space h="sm" />
|
<Space h="sm" />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
|
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
|
||||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||||
<Box className='center-flex' >
|
<Box className='center-flex' >
|
||||||
{isMedium?"General stats:":null}
|
{isMedium?"General stats:":null}
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
|
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
|
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
</Box>
|
</Box>
|
||||||
{isMedium?null:<Space h="md" />}
|
{isMedium?null:<Space h="md" />}
|
||||||
<Box className='center-flex' >
|
<Box className='center-flex' >
|
||||||
{ srv?
|
{ srv?
|
||||||
<Tooltip label="Add a new regex" position='bottom' color="blue">
|
<Tooltip label="Add a new regex" position='bottom' color="blue">
|
||||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled"
|
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled"
|
||||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<DocsButton doc="nfregex" />
|
<DocsButton doc="nfregex" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Box className="center-flex-row" style={{gap: 20}}>
|
<Box className="center-flex-row" style={{gap: 20}}>
|
||||||
{srv?null:<>
|
{srv?null:<>
|
||||||
<LoadingOverlay visible={services.isLoading} />
|
<LoadingOverlay visible={services.isLoading} />
|
||||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||||
navigator("/nfregex/"+srv.service_id)
|
navigator("/nfregex/"+srv.service_id)
|
||||||
}} />):<>
|
}} />):<>
|
||||||
<Box className='center-flex-row'>
|
<Box className='center-flex-row'>
|
||||||
<Space h="xl" />
|
<Space h="xl" />
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter Regex allows you to filter traffic using regexes</Title>
|
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter Regex allows you to filter traffic using regexes</Title>
|
||||||
<Space h="xs" />
|
<Space h="xs" />
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start a service, add your regexes and it's already done!</Title>
|
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start a service, add your regexes and it's already done!</Title>
|
||||||
<Space h="lg" />
|
<Space h="lg" />
|
||||||
<Box className='center-flex' style={{gap: 20}}>
|
<Box className='center-flex' style={{gap: 20}}>
|
||||||
<Tooltip label="Add a new service" color="blue">
|
<Tooltip label="Add a new service" color="blue">
|
||||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||||
<BsPlusLg size="20px" />
|
<BsPlusLg size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DocsButton doc="nfregex" size="xl" />
|
<DocsButton doc="nfregex" size="xl" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>}
|
</>}
|
||||||
</>}
|
</>}
|
||||||
</Box>
|
</Box>
|
||||||
{srv?children:null}
|
{srv?children:null}
|
||||||
{srv?
|
{srv?
|
||||||
<AddNewRegex opened={open} onClose={closeModal} service={srv} />:
|
<AddNewRegex opened={open} onClose={closeModal} service={srv} />:
|
||||||
<AddEditService opened={open} onClose={closeModal} />
|
<AddEditService opened={open} onClose={closeModal} />
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NFRegex;
|
export default NFRegex;
|
||||||
|
|||||||
@@ -1,77 +1,77 @@
|
|||||||
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { BsPlusLg } from "react-icons/bs";
|
import { BsPlusLg } from "react-icons/bs";
|
||||||
import ServiceRow from '../../components/PortHijack/ServiceRow';
|
import ServiceRow from '../../components/PortHijack/ServiceRow';
|
||||||
import { porthijackServiceQuery } from '../../components/PortHijack/utils';
|
import { porthijackServiceQuery } from '../../components/PortHijack/utils';
|
||||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||||
import AddNewService from '../../components/PortHijack/AddNewService';
|
import AddNewService from '../../components/PortHijack/AddNewService';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { TbReload } from 'react-icons/tb';
|
import { TbReload } from 'react-icons/tb';
|
||||||
import { FaServer } from 'react-icons/fa';
|
import { FaServer } from 'react-icons/fa';
|
||||||
import { GrDirections } from 'react-icons/gr';
|
import { GrDirections } from 'react-icons/gr';
|
||||||
import { DocsButton } from '../../components/DocsButton';
|
import { DocsButton } from '../../components/DocsButton';
|
||||||
|
|
||||||
|
|
||||||
function PortHijack() {
|
function PortHijack() {
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const isMedium = isMediumScreen()
|
const isMedium = isMediumScreen()
|
||||||
|
|
||||||
const services = porthijackServiceQuery()
|
const services = porthijackServiceQuery()
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(services.isError)
|
if(services.isError)
|
||||||
errorNotify("Porthijack Update failed!", getErrorMessage(services.error))
|
errorNotify("Porthijack Update failed!", getErrorMessage(services.error))
|
||||||
},[services.isError])
|
},[services.isError])
|
||||||
|
|
||||||
const closeModal = () => {setOpen(false);}
|
const closeModal = () => {setOpen(false);}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Space h="sm" />
|
<Space h="sm" />
|
||||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
|
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
|
||||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
<Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label="Add a new service" position='bottom' color="blue">
|
<Tooltip label="Add a new service" position='bottom' color="blue">
|
||||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
|
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
|
||||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<DocsButton doc="porthijack" />
|
<DocsButton doc="porthijack" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Box className="center-flex-row" style={{gap: 20}}>
|
<Box className="center-flex-row" style={{gap: 20}}>
|
||||||
<LoadingOverlay visible={services.isLoading} />
|
<LoadingOverlay visible={services.isLoading} />
|
||||||
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
|
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
|
||||||
<Box className='center-flex-row'>
|
<Box className='center-flex-row'>
|
||||||
<Space h="xl" />
|
<Space h="xl" />
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config</Title>
|
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config</Title>
|
||||||
<Space h="xs" />
|
<Space h="xs" />
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback</Title>
|
<Title className='center-flex' style={{textAlign:"center"}} order={5}>It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback</Title>
|
||||||
<Space h="xs" />
|
<Space h="xs" />
|
||||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs</Title>
|
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs</Title>
|
||||||
<Space h="lg" />
|
<Space h="lg" />
|
||||||
<Box className='center-flex' style={{gap: 20}}>
|
<Box className='center-flex' style={{gap: 20}}>
|
||||||
<Tooltip label="Add a new service" color="blue">
|
<Tooltip label="Add a new service" color="blue">
|
||||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||||
<BsPlusLg size="20px" />
|
<BsPlusLg size="20px" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<DocsButton doc="porthijack" size="xl" />
|
<DocsButton doc="porthijack" size="xl" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>}
|
</>}
|
||||||
<AddNewService opened={open} onClose={closeModal} />
|
<AddNewService opened={open} onClose={closeModal} />
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PortHijack;
|
export default PortHijack;
|
||||||
|
|||||||
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": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
|
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": false,
|
"esModuleInterop": false,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
run.py
30
run.py
@@ -254,9 +254,7 @@ def is_linux():
|
|||||||
def get_web_interface_url():
|
def get_web_interface_url():
|
||||||
if args.socket_dir:
|
if args.socket_dir:
|
||||||
return os.path.join(args.socket_dir, "firegex.sock")
|
return os.path.join(args.socket_dir, "firegex.sock")
|
||||||
|
|
||||||
# Per altre piattaforme, usiamo l'host configurato se non è 0.0.0.0
|
|
||||||
# altrimenti usiamo localhost per evitare confusione
|
|
||||||
display_host = "localhost" if args.host == "0.0.0.0" else args.host
|
display_host = "localhost" if args.host == "0.0.0.0" else args.host
|
||||||
return f"http://{display_host}:{args.port}"
|
return f"http://{display_host}:{args.port}"
|
||||||
|
|
||||||
@@ -270,14 +268,14 @@ def write_compose(skip_password = True):
|
|||||||
"firewall": {
|
"firewall": {
|
||||||
"restart": "unless-stopped",
|
"restart": "unless-stopped",
|
||||||
"container_name": "firegex",
|
"container_name": "firegex",
|
||||||
"build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}",
|
"build" if g.build else "image": "." if g.build else f"ghcr.io/ilyastar9999/firegex:{args.version}",
|
||||||
"network_mode": "host",
|
"network_mode": "host",
|
||||||
"environment": [
|
"environment": [
|
||||||
f"PORT={args.port}",
|
f"PORT={args.port}",
|
||||||
f"HOST={args.host}",
|
f"HOST={args.host}",
|
||||||
f"NTHREADS={args.threads}",
|
f"NTHREADS={args.threads}",
|
||||||
*([f"PSW_HASH_SET={hash_psw(psw_set)}"] if psw_set else []),
|
*([f"PSW_HASH_SET={hash_psw(psw_set)}"] if psw_set else []),
|
||||||
*([f"SOCKET_DIR=/run/firegex"] if args.socket_dir else [])
|
*(["SOCKET_DIR=/run/firegex"] if args.socket_dir else [])
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
"firegex_data:/execute/db",
|
"firegex_data:/execute/db",
|
||||||
@@ -325,7 +323,7 @@ def write_compose(skip_password = True):
|
|||||||
"container_name": "firegex",
|
"container_name": "firegex",
|
||||||
"build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}",
|
"build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}",
|
||||||
"ports": [
|
"ports": [
|
||||||
f"{args.host}:{args.port}:{args.port}"
|
f"{'' if args.host == 'any' else args.host+':'}{args.port}:{args.port}"
|
||||||
],
|
],
|
||||||
"environment": [
|
"environment": [
|
||||||
f"PORT={args.port}",
|
f"PORT={args.port}",
|
||||||
@@ -600,6 +598,10 @@ def cleanup_standalone_mounts():
|
|||||||
f"{g.rootfs_path}/sys_host/net.ipv6.conf.all.forwarding"
|
f"{g.rootfs_path}/sys_host/net.ipv6.conf.all.forwarding"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Add socket directory mount point if configured
|
||||||
|
if args.socket_dir:
|
||||||
|
mount_points.append(f"{g.rootfs_path}/run/firegex")
|
||||||
|
|
||||||
# Create umount commands (with || true to ignore errors)
|
# Create umount commands (with || true to ignore errors)
|
||||||
umount_commands = [f"umount -l {mount_point} || true" for mount_point in mount_points]
|
umount_commands = [f"umount -l {mount_point} || true" for mount_point in mount_points]
|
||||||
|
|
||||||
@@ -754,6 +756,18 @@ def setup_standalone_mounts():
|
|||||||
f"mount --bind /proc/sys/net/ipv6/conf/all/forwarding {g.rootfs_path}/sys_host/net.ipv6.conf.all.forwarding"
|
f"mount --bind /proc/sys/net/ipv6/conf/all/forwarding {g.rootfs_path}/sys_host/net.ipv6.conf.all.forwarding"
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# Add socket directory bind mount if configured
|
||||||
|
if args.socket_dir:
|
||||||
|
# Create socket directory on host if it doesn't exist
|
||||||
|
# Create mount point in rootfs and bind mount the socket directory
|
||||||
|
privileged_commands.extend([
|
||||||
|
f"mkdir -p {args.socket_dir}",
|
||||||
|
f"chmod 755 {args.socket_dir}",
|
||||||
|
f"mkdir -p {g.rootfs_path}/run/firegex",
|
||||||
|
f"chmod 755 {g.rootfs_path}/run/firegex",
|
||||||
|
f"mount --bind {args.socket_dir} {g.rootfs_path}/run/firegex"
|
||||||
|
])
|
||||||
|
|
||||||
# Run all privileged commands in one batch
|
# Run all privileged commands in one batch
|
||||||
if not run_privileged_commands(privileged_commands, "setup bind mounts"):
|
if not run_privileged_commands(privileged_commands, "setup bind mounts"):
|
||||||
puts("Failed to set up bind mounts", color=colors.red)
|
puts("Failed to set up bind mounts", color=colors.red)
|
||||||
@@ -784,9 +798,9 @@ def run_standalone():
|
|||||||
if psw_set:
|
if psw_set:
|
||||||
env_vars.append(f"PSW_HASH_SET={hash_psw(psw_set)}")
|
env_vars.append(f"PSW_HASH_SET={hash_psw(psw_set)}")
|
||||||
|
|
||||||
# Add socket dir if set
|
# Add socket dir if set (use path inside chroot)
|
||||||
if args.socket_dir:
|
if args.socket_dir:
|
||||||
env_vars.append(f"SOCKET_DIR={args.socket_dir}")
|
env_vars.append("SOCKET_DIR=/run/firegex")
|
||||||
|
|
||||||
# Prepare environment string for chroot
|
# Prepare environment string for chroot
|
||||||
env_string = " ".join([f"{var}" for var in env_vars])
|
env_string = " ".join([f"{var}" for var in env_vars])
|
||||||
|
|||||||
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": []
|
||||||
|
}
|
||||||
@@ -4,92 +4,126 @@ from utils.firegexapi import FiregexAPI
|
|||||||
import argparse
|
import argparse
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
if __name__ == "__main__":
|
||||||
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/")
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password')
|
parser.add_argument(
|
||||||
args = parser.parse_args()
|
"--address",
|
||||||
sep()
|
"-a",
|
||||||
puts("Testing will start on ", color=colors.cyan, end="")
|
type=str,
|
||||||
puts(f"{args.address}", color=colors.yellow)
|
required=False,
|
||||||
|
help="Address of firegex backend",
|
||||||
|
default="http://127.0.0.1:4444/",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--password", "-p", type=str, required=True, help="Firegex password"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
sep()
|
||||||
|
puts("Testing will start on ", color=colors.cyan, end="")
|
||||||
|
puts(f"{args.address}", color=colors.yellow)
|
||||||
|
|
||||||
firegex = FiregexAPI(args.address)
|
firegex = FiregexAPI(args.address)
|
||||||
|
|
||||||
#Connect to Firegex
|
# Connect to Firegex
|
||||||
if firegex.status()["status"] == "init":
|
if firegex.status()["status"] == "init":
|
||||||
if (firegex.set_password(args.password)):
|
if firegex.set_password(args.password):
|
||||||
puts(f"Sucessfully set password to {args.password} ✔", color=colors.green)
|
puts(f"Sucessfully set password to {args.password} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: Unknown response or password already put ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Unknown response or password already put ✗", color=colors.red)
|
if firegex.login(args.password):
|
||||||
exit(1)
|
puts("Sucessfully logged in ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
if (firegex.login(args.password)):
|
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||||
puts("Sucessfully logged in ✔", color=colors.green)
|
exit(1)
|
||||||
|
|
||||||
|
if firegex.status()["loggined"]:
|
||||||
|
puts("Correctly received status ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
puts("Test Failed: Unknown response or not logged in✗", color=colors.red)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
if(firegex.status()["loggined"]):
|
# Prepare second instance
|
||||||
puts("Correctly received status ✔", color=colors.green)
|
firegex2 = FiregexAPI(args.address)
|
||||||
else:
|
if firegex2.login(args.password):
|
||||||
puts("Test Failed: Unknown response or not logged in✗", color=colors.red)
|
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
||||||
exit(1)
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: Unknown response or wrong passowrd on second instance ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
#Prepare second instance
|
if firegex2.status()["loggined"]:
|
||||||
firegex2 = FiregexAPI(args.address)
|
puts("Correctly received status on second instance✔", color=colors.green)
|
||||||
if (firegex2.login(args.password)):
|
else:
|
||||||
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
puts(
|
||||||
else:
|
"Test Failed: Unknown response or not logged in on second instance✗",
|
||||||
puts("Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red)
|
color=colors.red,
|
||||||
exit(1)
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
if(firegex2.status()["loggined"]):
|
# Change password
|
||||||
puts("Correctly received status on second instance✔", color=colors.green)
|
new_password = secrets.token_hex(10)
|
||||||
else:
|
if firegex.change_password(new_password, expire=True):
|
||||||
puts("Test Failed: Unknown response or not logged in on second instance✗", color=colors.red)
|
puts(f"Sucessfully changed password to {new_password} ✔", color=colors.green)
|
||||||
exit(1)
|
else:
|
||||||
|
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
#Change password
|
# Check if we are still logged in
|
||||||
new_password = secrets.token_hex(10)
|
if firegex.status()["loggined"]:
|
||||||
if (firegex.change_password(new_password,expire=True)):
|
puts("Correctly received status after password change ✔", color=colors.green)
|
||||||
puts(f"Sucessfully changed password to {new_password} ✔", color=colors.green)
|
else:
|
||||||
else:
|
puts(
|
||||||
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
"Test Failed: Unknown response or not logged after password change ✗",
|
||||||
exit(1)
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
#Check if we are still logged in
|
# Check if second session expired and relog
|
||||||
if(firegex.status()["loggined"]):
|
|
||||||
puts("Correctly received status after password change ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Unknown response or not logged after password change ✗", color=colors.red)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
#Check if second session expired and relog
|
if not firegex2.status()["loggined"]:
|
||||||
|
puts("Second instance was expired currectly ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: Still logged in on second instance, expire expected ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
if firegex2.login(new_password):
|
||||||
|
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: Unknown response or wrong passowrd on second instance ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
if(not firegex2.status()["loggined"]):
|
# Change it back
|
||||||
puts("Second instance was expired currectly ✔", color=colors.green)
|
if firegex.change_password(args.password, expire=False):
|
||||||
else:
|
puts("Sucessfully restored the password ✔", color=colors.green)
|
||||||
puts("Test Failed: Still logged in on second instance, expire expected ✗", color=colors.red)
|
else:
|
||||||
exit(1)
|
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
||||||
if (firegex2.login(new_password)):
|
exit(1)
|
||||||
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
#Change it back
|
# Check if we are still logged in
|
||||||
if (firegex.change_password(args.password,expire=False)):
|
if firegex2.status()["loggined"]:
|
||||||
puts("Sucessfully restored the password ✔", color=colors.green)
|
puts("Correctly received status after password change ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
puts(
|
||||||
exit(1)
|
"Test Failed: Unknown response or not logged after password change ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
#Check if we are still logged in
|
puts("List of available interfaces:", color=colors.yellow)
|
||||||
if(firegex2.status()["loggined"]):
|
for interface in firegex.get_interfaces():
|
||||||
puts("Correctly received status after password change ✔", color=colors.green)
|
puts(
|
||||||
else:
|
"name: {}, address: {}".format(interface["name"], interface["addr"]),
|
||||||
puts("Test Failed: Unknown response or not logged after password change ✗", color=colors.red)
|
color=colors.yellow,
|
||||||
exit(1)
|
)
|
||||||
|
|
||||||
puts("List of available interfaces:", color=colors.yellow)
|
|
||||||
for interface in firegex.get_interfaces():
|
|
||||||
puts("name: {}, address: {}".format(interface["name"], interface["addr"]), color=colors.yellow)
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,247 +8,366 @@ import secrets
|
|||||||
import base64
|
import base64
|
||||||
import time
|
import time
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
if __name__ == "__main__":
|
||||||
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/")
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password')
|
parser.add_argument(
|
||||||
parser.add_argument("--service_name", "-n", type=str , required=False, help='Name of the test service', default="Test Service")
|
"--address",
|
||||||
parser.add_argument("--port", "-P", type=int , required=False, help='Port of the test service', default=1337)
|
"-a",
|
||||||
parser.add_argument("--ipv6", "-6" , action="store_true", help='Test Ipv6', default=False)
|
type=str,
|
||||||
parser.add_argument("--proto", "-m" , type=str, required=False, choices=["tcp","udp"], help='Select the protocol', default="tcp")
|
required=False,
|
||||||
|
help="Address of firegex backend",
|
||||||
|
default="http://127.0.0.1:4444/",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--password", "-p", type=str, required=True, help="Firegex password"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--service_name",
|
||||||
|
"-n",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
help="Name of the test service",
|
||||||
|
default="Test Service",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port",
|
||||||
|
"-P",
|
||||||
|
type=int,
|
||||||
|
required=False,
|
||||||
|
help="Port of the test service",
|
||||||
|
default=1337,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ipv6", "-6", action="store_true", help="Test Ipv6", default=False
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--proto",
|
||||||
|
"-m",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
choices=["tcp", "udp"],
|
||||||
|
help="Select the protocol",
|
||||||
|
default="tcp",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
sep()
|
sep()
|
||||||
puts("Testing will start on ", color=colors.cyan, end="")
|
puts("Testing will start on ", color=colors.cyan, end="")
|
||||||
puts(f"{args.address}", color=colors.yellow)
|
puts(f"{args.address}", color=colors.yellow)
|
||||||
|
|
||||||
firegex = FiregexAPI(args.address)
|
firegex = FiregexAPI(args.address)
|
||||||
|
|
||||||
#Login
|
# Login
|
||||||
if (firegex.login(args.password)):
|
if firegex.login(args.password):
|
||||||
puts("Sucessfully logged in ✔", color=colors.green)
|
puts("Sucessfully logged in ✔", color=colors.green)
|
||||||
else:
|
|
||||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
#Create server
|
|
||||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port,ipv6=args.ipv6)
|
|
||||||
|
|
||||||
def exit_test(code):
|
|
||||||
if service_id:
|
|
||||||
server.stop()
|
|
||||||
if(firegex.nfregex_delete_service(service_id)):
|
|
||||||
puts("Sucessfully deleted service ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
exit(code)
|
|
||||||
|
|
||||||
srvs = firegex.nfregex_get_services()
|
|
||||||
for ele in srvs:
|
|
||||||
if ele['name'] == args.service_name:
|
|
||||||
firegex.nfregex_delete_service(ele['service_id'])
|
|
||||||
|
|
||||||
service_id = firegex.nfregex_add_service(args.service_name, args.port, args.proto , "::1" if args.ipv6 else "127.0.0.1" )
|
|
||||||
if service_id:
|
|
||||||
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if(firegex.nfregex_start_service(service_id)):
|
|
||||||
puts("Sucessfully started service ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
server.start()
|
|
||||||
time.sleep(0.5)
|
|
||||||
try:
|
|
||||||
if server.sendCheckData(secrets.token_bytes(432)):
|
|
||||||
puts("Successfully tested first proxy with no regex ✔", color=colors.green)
|
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Data was corrupted ", color=colors.red)
|
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Create server
|
||||||
|
server = (TcpServer if args.proto == "tcp" else UdpServer)(
|
||||||
|
args.port, ipv6=args.ipv6
|
||||||
|
)
|
||||||
|
|
||||||
|
def exit_test(code):
|
||||||
|
if service_id:
|
||||||
|
server.stop()
|
||||||
|
if firegex.nfregex_delete_service(service_id):
|
||||||
|
puts("Sucessfully deleted service ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
exit(code)
|
||||||
|
|
||||||
|
srvs = firegex.nfregex_get_services()
|
||||||
|
for ele in srvs:
|
||||||
|
if ele["name"] == args.service_name:
|
||||||
|
firegex.nfregex_delete_service(ele["service_id"])
|
||||||
|
|
||||||
|
service_id = firegex.nfregex_add_service(
|
||||||
|
args.service_name, args.port, args.proto, "::1" if args.ipv6 else "127.0.0.1"
|
||||||
|
)
|
||||||
|
if service_id:
|
||||||
|
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
if firegex.nfregex_start_service(service_id):
|
||||||
|
puts("Sucessfully started service ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
except Exception:
|
|
||||||
puts("Test Failed: Couldn't send data to the server ", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
#Add new regex
|
|
||||||
secret = bytes(secrets.token_hex(16).encode())
|
|
||||||
|
|
||||||
if firegex.nfregex_add_regex(service_id,secret,"B",active=True,is_case_sensitive=True):
|
server.start()
|
||||||
puts(f"Sucessfully added regex {str(secret)} ✔", color=colors.green)
|
time.sleep(0.5)
|
||||||
else:
|
try:
|
||||||
puts(f"Test Failed: Couldn't add the regex {str(secret)} ✗", color=colors.red)
|
if server.sendCheckData(secrets.token_bytes(432)):
|
||||||
exit_test(1)
|
puts("Successfully tested first proxy with no regex ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Data was corrupted ", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
except Exception:
|
||||||
|
puts("Test Failed: Couldn't send data to the server ", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
# Add new regex
|
||||||
|
secret = bytes(secrets.token_hex(16).encode())
|
||||||
|
|
||||||
|
if firegex.nfregex_add_regex(
|
||||||
|
service_id, secret, "B", active=True, is_case_sensitive=True
|
||||||
|
):
|
||||||
|
puts(f"Sucessfully added regex {str(secret)} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts(f"Test Failed: Couldn't add the regex {str(secret)} ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
#Check if regex is present in the service
|
# Check if regex is present in the service
|
||||||
n_blocked = 0
|
n_blocked = 0
|
||||||
|
|
||||||
def getMetric(metric_name, regex):
|
def getMetric(metric_name, regex):
|
||||||
for metric in firegex.nfregex_get_metrics().split("\n"):
|
for metric in firegex.nfregex_get_metrics().split("\n"):
|
||||||
if metric.startswith(metric_name + "{") and f'regex="{regex}"' in metric:
|
if metric.startswith(metric_name + "{") and f'regex="{regex}"' in metric:
|
||||||
return int(metric.split(" ")[-1])
|
return int(metric.split(" ")[-1])
|
||||||
|
|
||||||
def checkRegex(regex, should_work=True, upper=False, deleted=False):
|
def checkRegex(regex, should_work=True, upper=False, deleted=False):
|
||||||
if should_work:
|
if should_work:
|
||||||
|
global n_blocked
|
||||||
|
for r in firegex.nfregex_get_service_regexes(service_id):
|
||||||
|
if r["regex"] == secret:
|
||||||
|
# Test the regex
|
||||||
|
s = regex.upper() if upper else regex
|
||||||
|
if not server.sendCheckData(
|
||||||
|
secrets.token_bytes(40) + s + secrets.token_bytes(40)
|
||||||
|
):
|
||||||
|
puts(
|
||||||
|
"The malicious request was successfully blocked ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
|
n_blocked += 1
|
||||||
|
time.sleep(1)
|
||||||
|
if firegex.nfregex_get_regex(r["id"])["n_packets"] == n_blocked:
|
||||||
|
puts(
|
||||||
|
"The packet was reported as blocked in the API ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: The packet wasn't reported as blocked in the API ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit_test(1)
|
||||||
|
if (
|
||||||
|
getMetric("firegex_blocked_packets", secret.decode())
|
||||||
|
== n_blocked
|
||||||
|
):
|
||||||
|
puts(
|
||||||
|
"The packet was reported as blocked in the metrics ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: The packet wasn't reported as blocked in the metrics ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit_test(1)
|
||||||
|
if getMetric("firegex_active", secret.decode()) == 1:
|
||||||
|
puts(
|
||||||
|
"The regex was reported as active in the metrics ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: The regex wasn't reported as active in the metrics ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit_test(1)
|
||||||
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: The request wasn't blocked ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit_test(1)
|
||||||
|
return
|
||||||
|
puts("Test Failed: The regex wasn't found ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
else:
|
||||||
|
if server.sendCheckData(
|
||||||
|
secrets.token_bytes(40)
|
||||||
|
+ base64.b64decode(regex)
|
||||||
|
+ secrets.token_bytes(40)
|
||||||
|
):
|
||||||
|
puts("The request wasn't blocked ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: The request was blocked when it shouldn't have",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit_test(1)
|
||||||
|
if not deleted:
|
||||||
|
if getMetric("firegex_active", secret.decode()) == 0:
|
||||||
|
puts(
|
||||||
|
"The regex was reported as inactive in the metrics ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: The regex wasn't reported as inactive in the metrics ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
def clear_regexes():
|
||||||
global n_blocked
|
global n_blocked
|
||||||
|
n_blocked = 0
|
||||||
for r in firegex.nfregex_get_service_regexes(service_id):
|
for r in firegex.nfregex_get_service_regexes(service_id):
|
||||||
if r["regex"] == secret:
|
if r["regex"] == secret:
|
||||||
#Test the regex
|
if firegex.nfregex_delete_regex(r["id"]):
|
||||||
s = regex.upper() if upper else regex
|
puts(
|
||||||
if not server.sendCheckData(secrets.token_bytes(40) + s + secrets.token_bytes(40)):
|
f"Sucessfully deleted regex with id {r['id']} ✔",
|
||||||
puts("The malicious request was successfully blocked ✔", color=colors.green)
|
color=colors.green,
|
||||||
n_blocked += 1
|
)
|
||||||
time.sleep(1)
|
|
||||||
if firegex.nfregex_get_regex(r["id"])["n_packets"] == n_blocked:
|
|
||||||
puts("The packet was reported as blocked in the API ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: The packet wasn't reported as blocked in the API ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
if getMetric("firegex_blocked_packets", secret.decode()) == n_blocked:
|
|
||||||
puts("The packet was reported as blocked in the metrics ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: The packet wasn't reported as blocked in the metrics ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
if getMetric("firegex_active", secret.decode()) == 1:
|
|
||||||
puts("The regex was reported as active in the metrics ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: The regex wasn't reported as active in the metrics ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: The request wasn't blocked ✗", color=colors.red)
|
puts("Test Failed: Coulnd't delete the regex ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
return
|
break
|
||||||
puts("Test Failed: The regex wasn't found ✗", color=colors.red)
|
if f'regex="{secret.decode()}"' not in firegex.nfregex_get_metrics():
|
||||||
exit_test(1)
|
puts("No regex metrics after deletion ✔", color=colors.green)
|
||||||
else:
|
|
||||||
if server.sendCheckData(secrets.token_bytes(40) + base64.b64decode(regex) + secrets.token_bytes(40)):
|
|
||||||
puts("The request wasn't blocked ✔", color=colors.green)
|
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: The request was blocked when it shouldn't have", color=colors.red)
|
puts(
|
||||||
|
"Test Failed: Metrics found after deleting the regex ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
if not deleted:
|
|
||||||
if getMetric("firegex_active", secret.decode()) == 0:
|
|
||||||
puts("The regex was reported as inactive in the metrics ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: The regex wasn't reported as inactive in the metrics ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
def clear_regexes():
|
checkRegex(secret)
|
||||||
global n_blocked
|
|
||||||
n_blocked = 0
|
# Pause the proxy
|
||||||
|
if firegex.nfregex_stop_service(service_id):
|
||||||
|
puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't pause the service ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# Check if it's actually paused
|
||||||
|
checkRegex(secret, should_work=False)
|
||||||
|
|
||||||
|
# Start firewall
|
||||||
|
if firegex.nfregex_start_service(service_id):
|
||||||
|
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
checkRegex(secret)
|
||||||
|
|
||||||
|
# Disable regex
|
||||||
for r in firegex.nfregex_get_service_regexes(service_id):
|
for r in firegex.nfregex_get_service_regexes(service_id):
|
||||||
if r["regex"] == secret:
|
if r["regex"] == secret:
|
||||||
if(firegex.nfregex_delete_regex(r["id"])):
|
if firegex.nfregex_disable_regex(r["id"]):
|
||||||
puts(f"Sucessfully deleted regex with id {r['id']} ✔", color=colors.green)
|
puts(
|
||||||
else:
|
f"Sucessfully disabled regex with id {r['id']} ✔",
|
||||||
puts("Test Failed: Coulnd't delete the regex ✗", color=colors.red)
|
color=colors.green,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't disable the regex ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
break
|
break
|
||||||
if f'regex="{secret.decode()}"' not in firegex.nfregex_get_metrics():
|
|
||||||
puts("No regex metrics after deletion ✔", color=colors.green)
|
# Check if it's actually disabled
|
||||||
|
checkRegex(secret, should_work=False)
|
||||||
|
|
||||||
|
# Enable regex
|
||||||
|
for r in firegex.nfregex_get_service_regexes(service_id):
|
||||||
|
if r["regex"] == secret:
|
||||||
|
if firegex.nfregex_enable_regex(r["id"]):
|
||||||
|
puts(
|
||||||
|
f"Sucessfully enabled regex with id {r['id']} ✔", color=colors.green
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't enable the regex ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
break
|
||||||
|
|
||||||
|
checkRegex(secret)
|
||||||
|
|
||||||
|
# Delete regex
|
||||||
|
clear_regexes()
|
||||||
|
|
||||||
|
# Check if it's actually deleted
|
||||||
|
checkRegex(secret, should_work=False, deleted=True)
|
||||||
|
|
||||||
|
# Add case insensitive regex
|
||||||
|
if firegex.nfregex_add_regex(
|
||||||
|
service_id, secret, "B", active=True, is_case_sensitive=False
|
||||||
|
):
|
||||||
|
puts(
|
||||||
|
f"Sucessfully added case insensitive regex {str(secret)} ✔",
|
||||||
|
color=colors.green,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Metrics found after deleting the regex ✗", color=colors.red)
|
puts(
|
||||||
|
f"Test Failed: Coulnd't add the case insensitive regex {str(secret)} ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
checkRegex(secret)
|
checkRegex(secret, upper=True)
|
||||||
|
checkRegex(secret)
|
||||||
|
|
||||||
#Pause the proxy
|
clear_regexes()
|
||||||
if(firegex.nfregex_stop_service(service_id)):
|
|
||||||
puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't pause the service ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
#Check if it's actually paused
|
# Rename service
|
||||||
checkRegex(secret,should_work=False)
|
if firegex.nfregex_rename_service(service_id, f"{args.service_name}2"):
|
||||||
|
puts(
|
||||||
#Start firewall
|
f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green
|
||||||
if(firegex.nfregex_start_service(service_id)):
|
)
|
||||||
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
checkRegex(secret)
|
|
||||||
|
|
||||||
#Disable regex
|
|
||||||
for r in firegex.nfregex_get_service_regexes(service_id):
|
|
||||||
if r["regex"] == secret:
|
|
||||||
if(firegex.nfregex_disable_regex(r["id"])):
|
|
||||||
puts(f"Sucessfully disabled regex with id {r['id']} ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't disable the regex ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
break
|
|
||||||
|
|
||||||
#Check if it's actually disabled
|
|
||||||
checkRegex(secret,should_work=False)
|
|
||||||
|
|
||||||
#Enable regex
|
|
||||||
for r in firegex.nfregex_get_service_regexes(service_id):
|
|
||||||
if r["regex"] == secret:
|
|
||||||
if(firegex.nfregex_enable_regex(r["id"])):
|
|
||||||
puts(f"Sucessfully enabled regex with id {r['id']} ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't enable the regex ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
break
|
|
||||||
|
|
||||||
checkRegex(secret)
|
|
||||||
|
|
||||||
#Delete regex
|
|
||||||
clear_regexes()
|
|
||||||
|
|
||||||
#Check if it's actually deleted
|
|
||||||
checkRegex(secret,should_work=False,deleted=True)
|
|
||||||
|
|
||||||
#Add case insensitive regex
|
|
||||||
if(firegex.nfregex_add_regex(service_id,secret,"B",active=True, is_case_sensitive=False)):
|
|
||||||
puts(f"Sucessfully added case insensitive regex {str(secret)} ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts(f"Test Failed: Coulnd't add the case insensitive regex {str(secret)} ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
checkRegex(secret, upper=True)
|
|
||||||
checkRegex(secret)
|
|
||||||
|
|
||||||
clear_regexes()
|
|
||||||
|
|
||||||
#Rename service
|
|
||||||
if(firegex.nfregex_rename_service(service_id,f"{args.service_name}2")):
|
|
||||||
puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
#Check if service was renamed correctly
|
|
||||||
service = firegex.nfregex_get_service(service_id)
|
|
||||||
if service["name"] == f"{args.service_name}2":
|
|
||||||
puts("Checked that service was renamed correctly ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
#Rename back service
|
|
||||||
if(firegex.nfregex_rename_service(service_id,f"{args.service_name}")):
|
|
||||||
puts(f"Sucessfully renamed service to {args.service_name} ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
#Change settings
|
|
||||||
opposite_proto = "udp" if args.proto == "tcp" else "tcp"
|
|
||||||
if(firegex.nfregex_settings_service(service_id, 1338, opposite_proto, "::dead:beef" if args.ipv6 else "123.123.123.123", True)):
|
|
||||||
srv_updated = firegex.nfregex_get_service(service_id)
|
|
||||||
if srv_updated["port"] == 1338 and srv_updated["proto"] == opposite_proto and ("::dead:beef" if args.ipv6 else "123.123.123.123") in srv_updated["ip_int"] and srv_updated["fail_open"]:
|
|
||||||
puts("Sucessfully changed service settings ✔", color=colors.green)
|
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Service settings weren't updated correctly ✗", color=colors.red)
|
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
exit_test(0)
|
# Check if service was renamed correctly
|
||||||
|
service = firegex.nfregex_get_service(service_id)
|
||||||
|
if service["name"] == f"{args.service_name}2":
|
||||||
|
puts("Checked that service was renamed correctly ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# Rename back service
|
||||||
|
if firegex.nfregex_rename_service(service_id, f"{args.service_name}"):
|
||||||
|
puts(
|
||||||
|
f"Sucessfully renamed service to {args.service_name} ✔", color=colors.green
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# Change settings
|
||||||
|
opposite_proto = "udp" if args.proto == "tcp" else "tcp"
|
||||||
|
if firegex.nfregex_settings_service(
|
||||||
|
service_id,
|
||||||
|
1338,
|
||||||
|
opposite_proto,
|
||||||
|
"::dead:beef" if args.ipv6 else "123.123.123.123",
|
||||||
|
True,
|
||||||
|
):
|
||||||
|
srv_updated = firegex.nfregex_get_service(service_id)
|
||||||
|
if (
|
||||||
|
srv_updated["port"] == 1338
|
||||||
|
and srv_updated["proto"] == opposite_proto
|
||||||
|
and ("::dead:beef" if args.ipv6 else "123.123.123.123")
|
||||||
|
in srv_updated["ip_int"]
|
||||||
|
and srv_updated["fail_open"]
|
||||||
|
):
|
||||||
|
puts("Sucessfully changed service settings ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts(
|
||||||
|
"Test Failed: Service settings weren't updated correctly ✗",
|
||||||
|
color=colors.red,
|
||||||
|
)
|
||||||
|
exit_test(1)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
exit_test(0)
|
||||||
|
|||||||
289
tests/ph_test.py
289
tests/ph_test.py
@@ -7,131 +7,180 @@ import argparse
|
|||||||
import secrets
|
import secrets
|
||||||
import time
|
import time
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
if __name__ == "__main__":
|
||||||
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/")
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password')
|
parser.add_argument(
|
||||||
parser.add_argument("--service_name", "-n", type=str , required=False, help='Name of the test service', default="Test Service")
|
"--address",
|
||||||
parser.add_argument("--port", "-P", type=int , required=False, help='Port of the test service', default=1337)
|
"-a",
|
||||||
parser.add_argument("--ipv6", "-6" , action="store_true", help='Test Ipv6', default=False)
|
type=str,
|
||||||
parser.add_argument("--proto", "-m" , type=str, required=False, choices=["tcp","udp"], help='Select the protocol', default="tcp")
|
required=False,
|
||||||
|
help="Address of firegex backend",
|
||||||
|
default="http://127.0.0.1:4444/",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--password", "-p", type=str, required=True, help="Firegex password"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--service_name",
|
||||||
|
"-n",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
help="Name of the test service",
|
||||||
|
default="Test Service",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--port",
|
||||||
|
"-P",
|
||||||
|
type=int,
|
||||||
|
required=False,
|
||||||
|
help="Port of the test service",
|
||||||
|
default=1337,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ipv6", "-6", action="store_true", help="Test Ipv6", default=False
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--proto",
|
||||||
|
"-m",
|
||||||
|
type=str,
|
||||||
|
required=False,
|
||||||
|
choices=["tcp", "udp"],
|
||||||
|
help="Select the protocol",
|
||||||
|
default="tcp",
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
sep()
|
sep()
|
||||||
puts("Testing will start on ", color=colors.cyan, end="")
|
puts("Testing will start on ", color=colors.cyan, end="")
|
||||||
puts(f"{args.address}", color=colors.yellow)
|
puts(f"{args.address}", color=colors.yellow)
|
||||||
|
|
||||||
firegex = FiregexAPI(args.address)
|
firegex = FiregexAPI(args.address)
|
||||||
|
|
||||||
#Login
|
# Login
|
||||||
if (firegex.login(args.password)):
|
if firegex.login(args.password):
|
||||||
puts("Sucessfully logged in ✔", color=colors.green)
|
puts("Sucessfully logged in ✔", color=colors.green)
|
||||||
else:
|
|
||||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
#Create server
|
|
||||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port+1,ipv6=args.ipv6,proxy_port=args.port)
|
|
||||||
|
|
||||||
def exit_test(code):
|
|
||||||
if service_id:
|
|
||||||
server.stop()
|
|
||||||
if(firegex.ph_delete_service(service_id)):
|
|
||||||
puts("Sucessfully deleted service ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
exit(code)
|
|
||||||
|
|
||||||
srvs = firegex.ph_get_services()
|
|
||||||
for ele in srvs:
|
|
||||||
if ele['name'] == args.service_name:
|
|
||||||
firegex.ph_delete_service(ele['service_id'])
|
|
||||||
|
|
||||||
#Create and start serivce
|
|
||||||
service_id = firegex.ph_add_service(args.service_name, args.port, args.port+1, args.proto , "::1" if args.ipv6 else "127.0.0.1", "::1" if args.ipv6 else "127.0.0.1")
|
|
||||||
if service_id:
|
|
||||||
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if(firegex.ph_start_service(service_id)):
|
|
||||||
puts("Sucessfully started service ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
server.start()
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
#Check if it started
|
|
||||||
def checkData(should_work):
|
|
||||||
res = None
|
|
||||||
try:
|
|
||||||
res = server.sendCheckData(secrets.token_bytes(432))
|
|
||||||
except (ConnectionRefusedError, TimeoutError):
|
|
||||||
res = None
|
|
||||||
if res:
|
|
||||||
if should_work:
|
|
||||||
puts("Successfully received data ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Connection wasn't blocked ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
else:
|
else:
|
||||||
if should_work:
|
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||||
puts("Test Failed: Data wans't received ✗", color=colors.red)
|
exit(1)
|
||||||
exit_test(1)
|
|
||||||
|
# Create server
|
||||||
|
server = (TcpServer if args.proto == "tcp" else UdpServer)(
|
||||||
|
args.port + 1, ipv6=args.ipv6, proxy_port=args.port
|
||||||
|
)
|
||||||
|
|
||||||
|
def exit_test(code):
|
||||||
|
if service_id:
|
||||||
|
server.stop()
|
||||||
|
if firegex.ph_delete_service(service_id):
|
||||||
|
puts("Sucessfully deleted service ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
exit(code)
|
||||||
|
|
||||||
|
srvs = firegex.ph_get_services()
|
||||||
|
for ele in srvs:
|
||||||
|
if ele["name"] == args.service_name:
|
||||||
|
firegex.ph_delete_service(ele["service_id"])
|
||||||
|
|
||||||
|
# Create and start serivce
|
||||||
|
service_id = firegex.ph_add_service(
|
||||||
|
args.service_name,
|
||||||
|
args.port,
|
||||||
|
args.port + 1,
|
||||||
|
args.proto,
|
||||||
|
"::1" if args.ipv6 else "127.0.0.1",
|
||||||
|
"::1" if args.ipv6 else "127.0.0.1",
|
||||||
|
)
|
||||||
|
if service_id:
|
||||||
|
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
if firegex.ph_start_service(service_id):
|
||||||
|
puts("Sucessfully started service ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
server.start()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
# Check if it started
|
||||||
|
def checkData(should_work):
|
||||||
|
res = None
|
||||||
|
try:
|
||||||
|
res = server.sendCheckData(secrets.token_bytes(432))
|
||||||
|
except (ConnectionRefusedError, TimeoutError):
|
||||||
|
res = None
|
||||||
|
if res:
|
||||||
|
if should_work:
|
||||||
|
puts("Successfully received data ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Connection wasn't blocked ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
else:
|
else:
|
||||||
puts("Successfully blocked connection ✔", color=colors.green)
|
if should_work:
|
||||||
|
puts("Test Failed: Data wans't received ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
else:
|
||||||
|
puts("Successfully blocked connection ✔", color=colors.green)
|
||||||
|
|
||||||
checkData(True)
|
checkData(True)
|
||||||
|
|
||||||
#Pause the proxy
|
# Pause the proxy
|
||||||
if(firegex.ph_stop_service(service_id)):
|
if firegex.ph_stop_service(service_id):
|
||||||
puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green)
|
puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green)
|
||||||
else:
|
else:
|
||||||
puts("Test Failed: Coulnd't pause the service ✗", color=colors.red)
|
puts("Test Failed: Coulnd't pause the service ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
checkData(False)
|
||||||
|
|
||||||
|
# Start firewall
|
||||||
|
if firegex.ph_start_service(service_id):
|
||||||
|
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
checkData(True)
|
||||||
|
|
||||||
|
# Change port
|
||||||
|
if firegex.ph_change_destination(
|
||||||
|
service_id, "::1" if args.ipv6 else "127.0.0.1", args.port + 2
|
||||||
|
):
|
||||||
|
puts("Sucessfully changed port ✔", color=colors.green)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't change destination ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
checkData(False)
|
||||||
|
|
||||||
|
server.stop()
|
||||||
|
server = (TcpServer if args.proto == "tcp" else UdpServer)(
|
||||||
|
args.port + 2, ipv6=args.ipv6, proxy_port=args.port
|
||||||
|
)
|
||||||
|
server.start()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
checkData(True)
|
||||||
|
|
||||||
|
# Rename service
|
||||||
|
if firegex.ph_rename_service(service_id, f"{args.service_name}2"):
|
||||||
|
puts(
|
||||||
|
f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||||
|
exit_test(1)
|
||||||
|
|
||||||
|
# Check if service was renamed correctly
|
||||||
|
for services in firegex.ph_get_services():
|
||||||
|
if services["name"] == f"{args.service_name}2":
|
||||||
|
puts("Checked that service was renamed correctly ✔", color=colors.green)
|
||||||
|
exit_test(0)
|
||||||
|
|
||||||
|
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
|
||||||
exit_test(1)
|
exit_test(1)
|
||||||
|
|
||||||
checkData(False)
|
|
||||||
|
|
||||||
#Start firewall
|
|
||||||
if(firegex.ph_start_service(service_id)):
|
|
||||||
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
checkData(True)
|
|
||||||
|
|
||||||
#Change port
|
|
||||||
if(firegex.ph_change_destination(service_id, "::1" if args.ipv6 else "127.0.0.1", args.port+2)):
|
|
||||||
puts("Sucessfully changed port ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't change destination ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
checkData(False)
|
|
||||||
|
|
||||||
server.stop()
|
|
||||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port+2,ipv6=args.ipv6,proxy_port=args.port)
|
|
||||||
server.start()
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
checkData(True)
|
|
||||||
|
|
||||||
#Rename service
|
|
||||||
if(firegex.ph_rename_service(service_id,f"{args.service_name}2")):
|
|
||||||
puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green)
|
|
||||||
else:
|
|
||||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|
||||||
#Check if service was renamed correctly
|
|
||||||
for services in firegex.ph_get_services():
|
|
||||||
if services["name"] == f"{args.service_name}2":
|
|
||||||
puts("Checked that service was renamed correctly ✔", color=colors.green)
|
|
||||||
exit_test(0)
|
|
||||||
|
|
||||||
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
|
|
||||||
exit_test(1)
|
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
0,4090.616
|
0,4090.616
|
||||||
1,2211.62
|
1,2211.62
|
||||||
2,1165.45
|
2,1165.45
|
||||||
3,849.39
|
3,849.39
|
||||||
4,828.635
|
4,828.635
|
||||||
5,741.537
|
5,741.537
|
||||||
6,632.721
|
6,632.721
|
||||||
7,624.772
|
7,624.772
|
||||||
8,529.234
|
8,529.234
|
||||||
9,469.688
|
9,469.688
|
||||||
10,336.33
|
10,336.33
|
||||||
11,427.783
|
11,427.783
|
||||||
12,400.662
|
12,400.662
|
||||||
13,335.086
|
13,335.086
|
||||||
14,342.042
|
14,342.042
|
||||||
15,307.283
|
15,307.283
|
||||||
16,239.694
|
16,239.694
|
||||||
17,295.163
|
17,295.163
|
||||||
18,285.787
|
18,285.787
|
||||||
19,254.402
|
19,254.402
|
||||||
20,250.553
|
20,250.553
|
||||||
21,227.146
|
21,227.146
|
||||||
22,238.747
|
22,238.747
|
||||||
23,234.718
|
23,234.718
|
||||||
24,210.484
|
24,210.484
|
||||||
25,210.697
|
25,210.697
|
||||||
26,205.943
|
26,205.943
|
||||||
27,202.568
|
27,202.568
|
||||||
28,194.341
|
28,194.341
|
||||||
29,189.916
|
29,189.916
|
||||||
30,154.228
|
30,154.228
|
||||||
31,168.922
|
31,168.922
|
||||||
32,173.623
|
32,173.623
|
||||||
33,125.431
|
33,125.431
|
||||||
34,162.154
|
34,162.154
|
||||||
35,149.865
|
35,149.865
|
||||||
36,150.088
|
36,150.088
|
||||||
37,146.085
|
37,146.085
|
||||||
38,137.182
|
38,137.182
|
||||||
39,138.686
|
39,138.686
|
||||||
40,136.302
|
40,136.302
|
||||||
41,132.707
|
41,132.707
|
||||||
42,100.928
|
42,100.928
|
||||||
43,126.414
|
43,126.414
|
||||||
44,125.271
|
44,125.271
|
||||||
45,117.839
|
45,117.839
|
||||||
46,89.494
|
46,89.494
|
||||||
47,116.939
|
47,116.939
|
||||||
48,112.517
|
48,112.517
|
||||||
49,111.369
|
49,111.369
|
||||||
50,108.568
|
50,108.568
|
||||||
|
|||||||
|
@@ -1,51 +1,51 @@
|
|||||||
0,3789.988
|
0,3789.988
|
||||||
1,2069.487
|
1,2069.487
|
||||||
2,1484.554
|
2,1484.554
|
||||||
3,956.972
|
3,956.972
|
||||||
4,1052.873
|
4,1052.873
|
||||||
5,739.658
|
5,739.658
|
||||||
6,534.722
|
6,534.722
|
||||||
7,638.524
|
7,638.524
|
||||||
8,573.833
|
8,573.833
|
||||||
9,531.658
|
9,531.658
|
||||||
10,476.167
|
10,476.167
|
||||||
11,443.746
|
11,443.746
|
||||||
12,406.027
|
12,406.027
|
||||||
13,385.739
|
13,385.739
|
||||||
14,341.563
|
14,341.563
|
||||||
15,318.699
|
15,318.699
|
||||||
16,303.722
|
16,303.722
|
||||||
17,284.924
|
17,284.924
|
||||||
18,284.336
|
18,284.336
|
||||||
19,267.32
|
19,267.32
|
||||||
20,202.74
|
20,202.74
|
||||||
21,243.849
|
21,243.849
|
||||||
22,226.082
|
22,226.082
|
||||||
23,214.348
|
23,214.348
|
||||||
24,216.8
|
24,216.8
|
||||||
25,188.98
|
25,188.98
|
||||||
26,158.68
|
26,158.68
|
||||||
27,166.556
|
27,166.556
|
||||||
28,148.287
|
28,148.287
|
||||||
29,149.681
|
29,149.681
|
||||||
30,177.043
|
30,177.043
|
||||||
31,175.321
|
31,175.321
|
||||||
32,165.312
|
32,165.312
|
||||||
33,166.943
|
33,166.943
|
||||||
34,159.026
|
34,159.026
|
||||||
35,156.759
|
35,156.759
|
||||||
36,150.216
|
36,150.216
|
||||||
37,144.932
|
37,144.932
|
||||||
38,146.088
|
38,146.088
|
||||||
39,135.897
|
39,135.897
|
||||||
40,136.99
|
40,136.99
|
||||||
41,128.557
|
41,128.557
|
||||||
42,100.307
|
42,100.307
|
||||||
43,103.249
|
43,103.249
|
||||||
44,123.49
|
44,123.49
|
||||||
45,120.39
|
45,120.39
|
||||||
46,118.055
|
46,118.055
|
||||||
47,115.0
|
47,115.0
|
||||||
48,112.593
|
48,112.593
|
||||||
49,109.55
|
49,109.55
|
||||||
50,109.512
|
50,109.512
|
||||||
|
|||||||
|
@@ -1,51 +1,51 @@
|
|||||||
0,4216.05
|
0,4216.05
|
||||||
1,4239.598
|
1,4239.598
|
||||||
2,2418.527
|
2,2418.527
|
||||||
3,2227.8
|
3,2227.8
|
||||||
4,2045.351
|
4,2045.351
|
||||||
5,2066.161
|
5,2066.161
|
||||||
6,2214.416
|
6,2214.416
|
||||||
7,2052.845
|
7,2052.845
|
||||||
8,2195.199
|
8,2195.199
|
||||||
9,2186.867
|
9,2186.867
|
||||||
10,2147.534
|
10,2147.534
|
||||||
11,2186.652
|
11,2186.652
|
||||||
12,2178.036
|
12,2178.036
|
||||||
13,2182.151
|
13,2182.151
|
||||||
14,2185.324
|
14,2185.324
|
||||||
15,1812.911
|
15,1812.911
|
||||||
16,2144.689
|
16,2144.689
|
||||||
17,2163.525
|
17,2163.525
|
||||||
18,2073.89
|
18,2073.89
|
||||||
19,2071.682
|
19,2071.682
|
||||||
20,2153.502
|
20,2153.502
|
||||||
21,2144.04
|
21,2144.04
|
||||||
22,2118.517
|
22,2118.517
|
||||||
23,2141.19
|
23,2141.19
|
||||||
24,2167.103
|
24,2167.103
|
||||||
25,2168.631
|
25,2168.631
|
||||||
26,2165.555
|
26,2165.555
|
||||||
27,2158.424
|
27,2158.424
|
||||||
28,2188.376
|
28,2188.376
|
||||||
29,2165.311
|
29,2165.311
|
||||||
30,2168.158
|
30,2168.158
|
||||||
31,2108.045
|
31,2108.045
|
||||||
32,2121.414
|
32,2121.414
|
||||||
33,2022.533
|
33,2022.533
|
||||||
34,1888.759
|
34,1888.759
|
||||||
35,2022.837
|
35,2022.837
|
||||||
36,2015.042
|
36,2015.042
|
||||||
37,1920.401
|
37,1920.401
|
||||||
38,2005.037
|
38,2005.037
|
||||||
39,2028.856
|
39,2028.856
|
||||||
40,2010.43
|
40,2010.43
|
||||||
41,1522.342
|
41,1522.342
|
||||||
42,1525.635
|
42,1525.635
|
||||||
43,1912.05
|
43,1912.05
|
||||||
44,1920.256
|
44,1920.256
|
||||||
45,1753.645
|
45,1753.645
|
||||||
46,1476.977
|
46,1476.977
|
||||||
47,1888.645
|
47,1888.645
|
||||||
48,1949.103
|
48,1949.103
|
||||||
49,1684.633
|
49,1684.633
|
||||||
50,1493.935
|
50,1493.935
|
||||||
|
|||||||
|
@@ -1,51 +1,51 @@
|
|||||||
0,4203.31
|
0,4203.31
|
||||||
1,4283.392
|
1,4283.392
|
||||||
2,2383.415
|
2,2383.415
|
||||||
3,2419.701
|
3,2419.701
|
||||||
4,2038.823
|
4,2038.823
|
||||||
5,2038.0
|
5,2038.0
|
||||||
6,2160.869
|
6,2160.869
|
||||||
7,2192.641
|
7,2192.641
|
||||||
8,2216.766
|
8,2216.766
|
||||||
9,2762.56
|
9,2762.56
|
||||||
10,2160.398
|
10,2160.398
|
||||||
11,2147.886
|
11,2147.886
|
||||||
12,2146.47
|
12,2146.47
|
||||||
13,2158.101
|
13,2158.101
|
||||||
14,2154.025
|
14,2154.025
|
||||||
15,1997.694
|
15,1997.694
|
||||||
16,2028.288
|
16,2028.288
|
||||||
17,2005.373
|
17,2005.373
|
||||||
18,2153.945
|
18,2153.945
|
||||||
19,2190.799
|
19,2190.799
|
||||||
20,2169.302
|
20,2169.302
|
||||||
21,2139.842
|
21,2139.842
|
||||||
22,2155.307
|
22,2155.307
|
||||||
23,2152.223
|
23,2152.223
|
||||||
24,2124.155
|
24,2124.155
|
||||||
25,2103.135
|
25,2103.135
|
||||||
26,2148.053
|
26,2148.053
|
||||||
27,2163.366
|
27,2163.366
|
||||||
28,2122.339
|
28,2122.339
|
||||||
29,2064.701
|
29,2064.701
|
||||||
30,2134.748
|
30,2134.748
|
||||||
31,1632.533
|
31,1632.533
|
||||||
32,2082.309
|
32,2082.309
|
||||||
33,1878.795
|
33,1878.795
|
||||||
34,2009.28
|
34,2009.28
|
||||||
35,1987.424
|
35,1987.424
|
||||||
36,1748.364
|
36,1748.364
|
||||||
37,1725.66
|
37,1725.66
|
||||||
38,1967.877
|
38,1967.877
|
||||||
39,1854.637
|
39,1854.637
|
||||||
40,1903.963
|
40,1903.963
|
||||||
41,1987.138
|
41,1987.138
|
||||||
42,1532.547
|
42,1532.547
|
||||||
43,1569.27
|
43,1569.27
|
||||||
44,1535.941
|
44,1535.941
|
||||||
45,1941.715
|
45,1941.715
|
||||||
46,2014.504
|
46,2014.504
|
||||||
47,2005.794
|
47,2005.794
|
||||||
48,2022.972
|
48,2022.972
|
||||||
49,1740.836
|
49,1740.836
|
||||||
50,1726.444
|
50,1726.444
|
||||||
|
|||||||
|
@@ -1,51 +1,51 @@
|
|||||||
0,710.619
|
0,710.619
|
||||||
1,887.877
|
1,887.877
|
||||||
2,981.431
|
2,981.431
|
||||||
3,1081.412
|
3,1081.412
|
||||||
4,1038.514
|
4,1038.514
|
||||||
5,1029.805
|
5,1029.805
|
||||||
6,928.317
|
6,928.317
|
||||||
7,1130.938
|
7,1130.938
|
||||||
8,1165.42
|
8,1165.42
|
||||||
9,925.632
|
9,925.632
|
||||||
10,949.483
|
10,949.483
|
||||||
11,1021.973
|
11,1021.973
|
||||||
12,903.878
|
12,903.878
|
||||||
13,1001.53
|
13,1001.53
|
||||||
14,895.351
|
14,895.351
|
||||||
15,1026.722
|
15,1026.722
|
||||||
16,634.727
|
16,634.727
|
||||||
17,744.758
|
17,744.758
|
||||||
18,978.59
|
18,978.59
|
||||||
19,962.375
|
19,962.375
|
||||||
20,997.471
|
20,997.471
|
||||||
21,929.785
|
21,929.785
|
||||||
22,1200.83
|
22,1200.83
|
||||||
23,1257.741
|
23,1257.741
|
||||||
24,772.729
|
24,772.729
|
||||||
25,683.913
|
25,683.913
|
||||||
26,1188.17
|
26,1188.17
|
||||||
27,919.961
|
27,919.961
|
||||||
28,922.225
|
28,922.225
|
||||||
29,1066.286
|
29,1066.286
|
||||||
30,979.399
|
30,979.399
|
||||||
31,978.917
|
31,978.917
|
||||||
32,988.415
|
32,988.415
|
||||||
33,1061.523
|
33,1061.523
|
||||||
34,942.85
|
34,942.85
|
||||||
35,1045.949
|
35,1045.949
|
||||||
36,883.941
|
36,883.941
|
||||||
37,958.41
|
37,958.41
|
||||||
38,989.523
|
38,989.523
|
||||||
39,1001.121
|
39,1001.121
|
||||||
40,1080.079
|
40,1080.079
|
||||||
41,1151.938
|
41,1151.938
|
||||||
42,1221.644
|
42,1221.644
|
||||||
43,991.855
|
43,991.855
|
||||||
44,1088.344
|
44,1088.344
|
||||||
45,973.641
|
45,973.641
|
||||||
46,952.35
|
46,952.35
|
||||||
47,1089.644
|
47,1089.644
|
||||||
48,939.615
|
48,939.615
|
||||||
49,1258.419
|
49,1258.419
|
||||||
50,949.414
|
50,949.414
|
||||||
|
|||||||
|
@@ -1,51 +1,51 @@
|
|||||||
0,3245.763
|
0,3245.763
|
||||||
1,3283.646
|
1,3283.646
|
||||||
2,3741.157
|
2,3741.157
|
||||||
3,3691.206
|
3,3691.206
|
||||||
4,3365.134
|
4,3365.134
|
||||||
5,3691.457
|
5,3691.457
|
||||||
6,3354.807
|
6,3354.807
|
||||||
7,3526.728
|
7,3526.728
|
||||||
8,3252.62
|
8,3252.62
|
||||||
9,3551.086
|
9,3551.086
|
||||||
10,3561.506
|
10,3561.506
|
||||||
11,3525.577
|
11,3525.577
|
||||||
12,2776.064
|
12,2776.064
|
||||||
13,3541.86
|
13,3541.86
|
||||||
14,3501.34
|
14,3501.34
|
||||||
15,3692.092
|
15,3692.092
|
||||||
16,3637.166
|
16,3637.166
|
||||||
17,3617.031
|
17,3617.031
|
||||||
18,3700.092
|
18,3700.092
|
||||||
19,3176.831
|
19,3176.831
|
||||||
20,3368.038
|
20,3368.038
|
||||||
21,3716.577
|
21,3716.577
|
||||||
22,3452.917
|
22,3452.917
|
||||||
23,3617.604
|
23,3617.604
|
||||||
24,3651.796
|
24,3651.796
|
||||||
25,3552.053
|
25,3552.053
|
||||||
26,3843.18
|
26,3843.18
|
||||||
27,3720.406
|
27,3720.406
|
||||||
28,3431.1
|
28,3431.1
|
||||||
29,3578.973
|
29,3578.973
|
||||||
30,3561.994
|
30,3561.994
|
||||||
31,3524.566
|
31,3524.566
|
||||||
32,3567.537
|
32,3567.537
|
||||||
33,3626.767
|
33,3626.767
|
||||||
34,3498.361
|
34,3498.361
|
||||||
35,3621.396
|
35,3621.396
|
||||||
36,3297.839
|
36,3297.839
|
||||||
37,3541.207
|
37,3541.207
|
||||||
38,3560.364
|
38,3560.364
|
||||||
39,3589.746
|
39,3589.746
|
||||||
40,3686.673
|
40,3686.673
|
||||||
41,3463.811
|
41,3463.811
|
||||||
42,3428.408
|
42,3428.408
|
||||||
43,3753.139
|
43,3753.139
|
||||||
44,3368.89
|
44,3368.89
|
||||||
45,3324.876
|
45,3324.876
|
||||||
46,3614.895
|
46,3614.895
|
||||||
47,3245.942
|
47,3245.942
|
||||||
48,3257.925
|
48,3257.925
|
||||||
49,3200.585
|
49,3200.585
|
||||||
50,3321.55
|
50,3321.55
|
||||||
|
|||||||
|
@@ -1,51 +1,51 @@
|
|||||||
0,1790.382
|
0,1790.382
|
||||||
1,1933.881
|
1,1933.881
|
||||||
2,1941.564
|
2,1941.564
|
||||||
3,1926.518
|
3,1926.518
|
||||||
4,1945.295
|
4,1945.295
|
||||||
5,1734.462
|
5,1734.462
|
||||||
6,2009.994
|
6,2009.994
|
||||||
7,2007.538
|
7,2007.538
|
||||||
8,2004.825
|
8,2004.825
|
||||||
9,1848.551
|
9,1848.551
|
||||||
10,1836.558
|
10,1836.558
|
||||||
11,1977.19
|
11,1977.19
|
||||||
12,1987.207
|
12,1987.207
|
||||||
13,2007.422
|
13,2007.422
|
||||||
14,1994.914
|
14,1994.914
|
||||||
15,1982.997
|
15,1982.997
|
||||||
16,1955.828
|
16,1955.828
|
||||||
17,1705.883
|
17,1705.883
|
||||||
18,1983.501
|
18,1983.501
|
||||||
19,1951.311
|
19,1951.311
|
||||||
20,1921.772
|
20,1921.772
|
||||||
21,1956.908
|
21,1956.908
|
||||||
22,1948.865
|
22,1948.865
|
||||||
23,1929.387
|
23,1929.387
|
||||||
24,1814.539
|
24,1814.539
|
||||||
25,2084.284
|
25,2084.284
|
||||||
26,1830.901
|
26,1830.901
|
||||||
27,1946.713
|
27,1946.713
|
||||||
28,1958.238
|
28,1958.238
|
||||||
29,1906.573
|
29,1906.573
|
||||||
30,1895.341
|
30,1895.341
|
||||||
31,1986.09
|
31,1986.09
|
||||||
32,1943.785
|
32,1943.785
|
||||||
33,1879.917
|
33,1879.917
|
||||||
34,1946.029
|
34,1946.029
|
||||||
35,1858.958
|
35,1858.958
|
||||||
36,2009.44
|
36,2009.44
|
||||||
37,1876.749
|
37,1876.749
|
||||||
38,1967.254
|
38,1967.254
|
||||||
39,1968.595
|
39,1968.595
|
||||||
40,1846.438
|
40,1846.438
|
||||||
41,1955.897
|
41,1955.897
|
||||||
42,1986.446
|
42,1986.446
|
||||||
43,1965.143
|
43,1965.143
|
||||||
44,1963.016
|
44,1963.016
|
||||||
45,1890.88
|
45,1890.88
|
||||||
46,1998.801
|
46,1998.801
|
||||||
47,1682.048
|
47,1682.048
|
||||||
48,2023.688
|
48,2023.688
|
||||||
49,1982.952
|
49,1982.952
|
||||||
50,1993.641
|
50,1993.641
|
||||||
|
|||||||
|
@@ -1,51 +1,51 @@
|
|||||||
0,4007.679
|
0,4007.679
|
||||||
1,3963.986
|
1,3963.986
|
||||||
2,4222.243
|
2,4222.243
|
||||||
3,3640.707
|
3,3640.707
|
||||||
4,4388.553
|
4,4388.553
|
||||||
5,3636.047
|
5,3636.047
|
||||||
6,3644.611
|
6,3644.611
|
||||||
7,3547.39
|
7,3547.39
|
||||||
8,3412.162
|
8,3412.162
|
||||||
9,3632.367
|
9,3632.367
|
||||||
10,3536.655
|
10,3536.655
|
||||||
11,3820.019
|
11,3820.019
|
||||||
12,3677.177
|
12,3677.177
|
||||||
13,3366.323
|
13,3366.323
|
||||||
14,3353.031
|
14,3353.031
|
||||||
15,3392.423
|
15,3392.423
|
||||||
16,3330.368
|
16,3330.368
|
||||||
17,3363.272
|
17,3363.272
|
||||||
18,4027.34
|
18,4027.34
|
||||||
19,3467.982
|
19,3467.982
|
||||||
20,3607.754
|
20,3607.754
|
||||||
21,3767.614
|
21,3767.614
|
||||||
22,3340.544
|
22,3340.544
|
||||||
23,4086.612
|
23,4086.612
|
||||||
24,3784.164
|
24,3784.164
|
||||||
25,3496.518
|
25,3496.518
|
||||||
26,3543.808
|
26,3543.808
|
||||||
27,3453.934
|
27,3453.934
|
||||||
28,3546.188
|
28,3546.188
|
||||||
29,3458.804
|
29,3458.804
|
||||||
30,3728.609
|
30,3728.609
|
||||||
31,3697.624
|
31,3697.624
|
||||||
32,3698.191
|
32,3698.191
|
||||||
33,3673.973
|
33,3673.973
|
||||||
34,3690.046
|
34,3690.046
|
||||||
35,3663.799
|
35,3663.799
|
||||||
36,3540.004
|
36,3540.004
|
||||||
37,3857.604
|
37,3857.604
|
||||||
38,3426.215
|
38,3426.215
|
||||||
39,3704.176
|
39,3704.176
|
||||||
40,3796.133
|
40,3796.133
|
||||||
41,3604.623
|
41,3604.623
|
||||||
42,3650.508
|
42,3650.508
|
||||||
43,3501.861
|
43,3501.861
|
||||||
44,3685.992
|
44,3685.992
|
||||||
45,3623.404
|
45,3623.404
|
||||||
46,3728.601
|
46,3728.601
|
||||||
47,3844.994
|
47,3844.994
|
||||||
48,3820.046
|
48,3820.046
|
||||||
49,3680.976
|
49,3680.976
|
||||||
50,3797.432
|
50,3797.432
|
||||||
|
|||||||
|
@@ -1,101 +1,101 @@
|
|||||||
No filters,test data
|
No filters,test data
|
||||||
1600.27,1772.897
|
1600.27,1772.897
|
||||||
1486.257,1455.93
|
1486.257,1455.93
|
||||||
1534.667,1403.539
|
1534.667,1403.539
|
||||||
1244.374,1665.846
|
1244.374,1665.846
|
||||||
1569.867,1627.449
|
1569.867,1627.449
|
||||||
1522.719,1084.153
|
1522.719,1084.153
|
||||||
1391.244,1259.783
|
1391.244,1259.783
|
||||||
1528.465,1282.901
|
1528.465,1282.901
|
||||||
1310.989,1275.515
|
1310.989,1275.515
|
||||||
1675.138,1074.39
|
1675.138,1074.39
|
||||||
1393.644,1359.139
|
1393.644,1359.139
|
||||||
1639.889,1162.937
|
1639.889,1162.937
|
||||||
1658.168,1239.767
|
1658.168,1239.767
|
||||||
1477.156,1308.195
|
1477.156,1308.195
|
||||||
1224.386,1298.007
|
1224.386,1298.007
|
||||||
1420.7,1087.031
|
1420.7,1087.031
|
||||||
1353.746,1090.502
|
1353.746,1090.502
|
||||||
1759.778,1179.381
|
1759.778,1179.381
|
||||||
1414.33,1222.86
|
1414.33,1222.86
|
||||||
1475.981,1295.207
|
1475.981,1295.207
|
||||||
1375.197,1327.8
|
1375.197,1327.8
|
||||||
1265.015,1189.121
|
1265.015,1189.121
|
||||||
1335.179,1594.98
|
1335.179,1594.98
|
||||||
1191.896,1271.873
|
1191.896,1271.873
|
||||||
1596.418,1100.372
|
1596.418,1100.372
|
||||||
1433.755,1147.945
|
1433.755,1147.945
|
||||||
1213.187,1312.989
|
1213.187,1312.989
|
||||||
1157.99,1153.825
|
1157.99,1153.825
|
||||||
1322.314,1184.481
|
1322.314,1184.481
|
||||||
1262.974,1271.012
|
1262.974,1271.012
|
||||||
1266.223,1350.519
|
1266.223,1350.519
|
||||||
1192.275,1199.142
|
1192.275,1199.142
|
||||||
1296.164,1189.432
|
1296.164,1189.432
|
||||||
1245.501,1185.107
|
1245.501,1185.107
|
||||||
1293.076,1374.689
|
1293.076,1374.689
|
||||||
1260.554,1384.055
|
1260.554,1384.055
|
||||||
1219.219,1420.395
|
1219.219,1420.395
|
||||||
1132.234,1099.141
|
1132.234,1099.141
|
||||||
1129.541,1101.805
|
1129.541,1101.805
|
||||||
1273.171,1210.564
|
1273.171,1210.564
|
||||||
1269.415,1184.094
|
1269.415,1184.094
|
||||||
1370.586,1321.974
|
1370.586,1321.974
|
||||||
1303.694,1317.074
|
1303.694,1317.074
|
||||||
1413.705,1380.092
|
1413.705,1380.092
|
||||||
1324.827,1142.097
|
1324.827,1142.097
|
||||||
1124.399,1548.557
|
1124.399,1548.557
|
||||||
1137.381,1029.353
|
1137.381,1029.353
|
||||||
1419.146,1326.829
|
1419.146,1326.829
|
||||||
1342.397,1270.316
|
1342.397,1270.316
|
||||||
1546.898,1258.933
|
1546.898,1258.933
|
||||||
1268.918,1062.23
|
1268.918,1062.23
|
||||||
1239.877,1234.887
|
1239.877,1234.887
|
||||||
1474.269,1181.184
|
1474.269,1181.184
|
||||||
1289.763,1139.728
|
1289.763,1139.728
|
||||||
1387.416,1125.734
|
1387.416,1125.734
|
||||||
1128.784,1278.381
|
1128.784,1278.381
|
||||||
1519.4,1243.597
|
1519.4,1243.597
|
||||||
1343.003,1153.18
|
1343.003,1153.18
|
||||||
1547.543,1117.816
|
1547.543,1117.816
|
||||||
1582.958,1594.145
|
1582.958,1594.145
|
||||||
1618.213,1358.087
|
1618.213,1358.087
|
||||||
1449.399,1295.487
|
1449.399,1295.487
|
||||||
1373.062,1174.153
|
1373.062,1174.153
|
||||||
1211.207,1346.833
|
1211.207,1346.833
|
||||||
1066.275,1417.633
|
1066.275,1417.633
|
||||||
1203.659,1131.727
|
1203.659,1131.727
|
||||||
1129.005,1351.061
|
1129.005,1351.061
|
||||||
1200.245,1615.952
|
1200.245,1615.952
|
||||||
1232.596,1250.436
|
1232.596,1250.436
|
||||||
1262.319,1563.46
|
1262.319,1563.46
|
||||||
1127.022,1651.89
|
1127.022,1651.89
|
||||||
1736.368,1561.661
|
1736.368,1561.661
|
||||||
1310.858,1459.713
|
1310.858,1459.713
|
||||||
1351.455,1608.494
|
1351.455,1608.494
|
||||||
1156.124,1440.379
|
1156.124,1440.379
|
||||||
1220.053,1267.708
|
1220.053,1267.708
|
||||||
1171.428,1300.284
|
1171.428,1300.284
|
||||||
1149.242,1087.453
|
1149.242,1087.453
|
||||||
1213.915,1081.207
|
1213.915,1081.207
|
||||||
1092.869,1402.761
|
1092.869,1402.761
|
||||||
1243.623,1321.907
|
1243.623,1321.907
|
||||||
1216.257,1217.721
|
1216.257,1217.721
|
||||||
1221.354,1263.695
|
1221.354,1263.695
|
||||||
1242.771,1241.684
|
1242.771,1241.684
|
||||||
1427.276,1322.01
|
1427.276,1322.01
|
||||||
1328.502,1346.21
|
1328.502,1346.21
|
||||||
1275.719,1269.909
|
1275.719,1269.909
|
||||||
1372.075,1451.069
|
1372.075,1451.069
|
||||||
1486.541,1532.56
|
1486.541,1532.56
|
||||||
1577.036,1539.804
|
1577.036,1539.804
|
||||||
1628.025,1372.806
|
1628.025,1372.806
|
||||||
1415.623,1239.201
|
1415.623,1239.201
|
||||||
1198.632,1095.849
|
1198.632,1095.849
|
||||||
1170.341,1255.875
|
1170.341,1255.875
|
||||||
1214.99,1424.292
|
1214.99,1424.292
|
||||||
1356.431,1135.588
|
1356.431,1135.588
|
||||||
1817.822,1212.386
|
1817.822,1212.386
|
||||||
1745.199,1170.863
|
1745.199,1170.863
|
||||||
1779.083,1145.458
|
1779.083,1145.458
|
||||||
1544.934,1076.386
|
1544.934,1076.386
|
||||||
|
|||||||
|
@@ -1,101 +1,101 @@
|
|||||||
No filters,test data
|
No filters,test data
|
||||||
2098.666,2118.781
|
2098.666,2118.781
|
||||||
2175.2,2086.957
|
2175.2,2086.957
|
||||||
2177.653,1795.287
|
2177.653,1795.287
|
||||||
1775.63,1745.066
|
1775.63,1745.066
|
||||||
1827.78,2038.921
|
1827.78,2038.921
|
||||||
1813.369,2179.81
|
1813.369,2179.81
|
||||||
1988.859,2176.883
|
1988.859,2176.883
|
||||||
1634.541,1704.071
|
1634.541,1704.071
|
||||||
1878.829,1869.999
|
1878.829,1869.999
|
||||||
1738.987,2024.959
|
1738.987,2024.959
|
||||||
1920.502,1477.726
|
1920.502,1477.726
|
||||||
1895.909,1732.832
|
1895.909,1732.832
|
||||||
1812.012,1850.978
|
1812.012,1850.978
|
||||||
1908.106,1902.953
|
1908.106,1902.953
|
||||||
2112.837,1726.547
|
2112.837,1726.547
|
||||||
1765.808,1710.915
|
1765.808,1710.915
|
||||||
1918.121,1900.619
|
1918.121,1900.619
|
||||||
1892.779,2054.93
|
1892.779,2054.93
|
||||||
1852.952,2113.928
|
1852.952,2113.928
|
||||||
1713.67,1770.379
|
1713.67,1770.379
|
||||||
1873.637,2011.518
|
1873.637,2011.518
|
||||||
1787.007,2104.061
|
1787.007,2104.061
|
||||||
1764.704,2134.151
|
1764.704,2134.151
|
||||||
2064.776,2073.226
|
2064.776,2073.226
|
||||||
1838.23,1762.436
|
1838.23,1762.436
|
||||||
1808.339,1792.41
|
1808.339,1792.41
|
||||||
1756.516,1706.501
|
1756.516,1706.501
|
||||||
1665.888,1610.771
|
1665.888,1610.771
|
||||||
1682.272,1650.033
|
1682.272,1650.033
|
||||||
1690.473,1563.995
|
1690.473,1563.995
|
||||||
1997.801,1955.53
|
1997.801,1955.53
|
||||||
1660.487,1669.25
|
1660.487,1669.25
|
||||||
2023.106,1727.046
|
2023.106,1727.046
|
||||||
1724.59,1686.137
|
1724.59,1686.137
|
||||||
1697.656,1627.136
|
1697.656,1627.136
|
||||||
1689.65,1571.13
|
1689.65,1571.13
|
||||||
1628.35,1699.239
|
1628.35,1699.239
|
||||||
1843.768,1825.739
|
1843.768,1825.739
|
||||||
1715.158,1573.695
|
1715.158,1573.695
|
||||||
1732.695,1875.656
|
1732.695,1875.656
|
||||||
1902.818,1968.505
|
1902.818,1968.505
|
||||||
1699.277,1919.737
|
1699.277,1919.737
|
||||||
1618.75,2015.258
|
1618.75,2015.258
|
||||||
1696.055,2014.261
|
1696.055,2014.261
|
||||||
1792.486,1606.754
|
1792.486,1606.754
|
||||||
1889.583,1625.965
|
1889.583,1625.965
|
||||||
1716.951,1572.049
|
1716.951,1572.049
|
||||||
1727.305,1649.502
|
1727.305,1649.502
|
||||||
1747.618,2099.787
|
1747.618,2099.787
|
||||||
1698.546,2153.363
|
1698.546,2153.363
|
||||||
1723.117,1637.074
|
1723.117,1637.074
|
||||||
1654.061,1721.968
|
1654.061,1721.968
|
||||||
1735.332,1587.906
|
1735.332,1587.906
|
||||||
1841.808,1565.797
|
1841.808,1565.797
|
||||||
2006.973,1665.615
|
2006.973,1665.615
|
||||||
1730.909,1883.505
|
1730.909,1883.505
|
||||||
1681.954,1553.826
|
1681.954,1553.826
|
||||||
1653.215,1849.824
|
1653.215,1849.824
|
||||||
2072.138,1990.474
|
2072.138,1990.474
|
||||||
1792.302,2176.718
|
1792.302,2176.718
|
||||||
1679.381,2128.083
|
1679.381,2128.083
|
||||||
1653.368,2078.013
|
1653.368,2078.013
|
||||||
1399.58,2065.031
|
1399.58,2065.031
|
||||||
1669.979,1815.553
|
1669.979,1815.553
|
||||||
1677.346,1870.055
|
1677.346,1870.055
|
||||||
1652.22,2010.441
|
1652.22,2010.441
|
||||||
1870.35,1687.893
|
1870.35,1687.893
|
||||||
1772.229,1857.193
|
1772.229,1857.193
|
||||||
1743.552,1813.027
|
1743.552,1813.027
|
||||||
1685.312,1466.505
|
1685.312,1466.505
|
||||||
1863.269,1813.398
|
1863.269,1813.398
|
||||||
1694.335,1889.661
|
1694.335,1889.661
|
||||||
1739.016,1740.381
|
1739.016,1740.381
|
||||||
1764.462,1752.725
|
1764.462,1752.725
|
||||||
1702.134,2069.289
|
1702.134,2069.289
|
||||||
1955.771,2176.617
|
1955.771,2176.617
|
||||||
2046.117,2137.499
|
2046.117,2137.499
|
||||||
1766.64,2177.955
|
1766.64,2177.955
|
||||||
1733.26,2148.497
|
1733.26,2148.497
|
||||||
1834.827,2161.573
|
1834.827,2161.573
|
||||||
2087.089,2119.311
|
2087.089,2119.311
|
||||||
2154.753,1679.596
|
2154.753,1679.596
|
||||||
2073.729,1912.012
|
2073.729,1912.012
|
||||||
2082.37,1841.045
|
2082.37,1841.045
|
||||||
2160.86,1813.257
|
2160.86,1813.257
|
||||||
1678.515,1894.864
|
1678.515,1894.864
|
||||||
1758.394,1884.985
|
1758.394,1884.985
|
||||||
1673.919,1732.373
|
1673.919,1732.373
|
||||||
1666.474,1737.66
|
1666.474,1737.66
|
||||||
1679.444,1463.082
|
1679.444,1463.082
|
||||||
1684.006,2002.343
|
1684.006,2002.343
|
||||||
1737.287,2026.394
|
1737.287,2026.394
|
||||||
1811.305,2084.689
|
1811.305,2084.689
|
||||||
2127.121,2117.391
|
2127.121,2117.391
|
||||||
2139.884,1984.606
|
2139.884,1984.606
|
||||||
1677.256,1770.76
|
1677.256,1770.76
|
||||||
1698.544,1833.011
|
1698.544,1833.011
|
||||||
1905.446,1734.777
|
1905.446,1734.777
|
||||||
1913.257,1688.401
|
1913.257,1688.401
|
||||||
2063.73,1667.27
|
2063.73,1667.27
|
||||||
|
|||||||
|
@@ -1,101 +1,101 @@
|
|||||||
No filters,test data
|
No filters,test data
|
||||||
3841.832,3177.356
|
3841.832,3177.356
|
||||||
3369.899,3819.926
|
3369.899,3819.926
|
||||||
3884.689,2843.759
|
3884.689,2843.759
|
||||||
3391.267,3106.399
|
3391.267,3106.399
|
||||||
3740.054,2899.246
|
3740.054,2899.246
|
||||||
3754.086,3254.525
|
3754.086,3254.525
|
||||||
3284.178,3180.96
|
3284.178,3180.96
|
||||||
3293.044,3356.928
|
3293.044,3356.928
|
||||||
3653.05,2925.883
|
3653.05,2925.883
|
||||||
3830.609,2784.715
|
3830.609,2784.715
|
||||||
3691.078,3283.715
|
3691.078,3283.715
|
||||||
3551.286,3437.899
|
3551.286,3437.899
|
||||||
3651.296,2759.088
|
3651.296,2759.088
|
||||||
3726.295,3289.184
|
3726.295,3289.184
|
||||||
3860.353,3067.069
|
3860.353,3067.069
|
||||||
3910.997,3764.354
|
3910.997,3764.354
|
||||||
3775.794,3182.171
|
3775.794,3182.171
|
||||||
3824.719,3376.774
|
3824.719,3376.774
|
||||||
3245.109,2954.582
|
3245.109,2954.582
|
||||||
3705.489,4101.548
|
3705.489,4101.548
|
||||||
3484.114,3155.55
|
3484.114,3155.55
|
||||||
3742.727,3153.767
|
3742.727,3153.767
|
||||||
3964.472,3624.241
|
3964.472,3624.241
|
||||||
3747.219,2787.965
|
3747.219,2787.965
|
||||||
3746.575,3518.095
|
3746.575,3518.095
|
||||||
3903.7,2942.676
|
3903.7,2942.676
|
||||||
3888.772,3222.041
|
3888.772,3222.041
|
||||||
3854.913,2479.502
|
3854.913,2479.502
|
||||||
3716.801,2876.082
|
3716.801,2876.082
|
||||||
3919.146,2748.543
|
3919.146,2748.543
|
||||||
3908.195,2742.45
|
3908.195,2742.45
|
||||||
3894.436,3135.703
|
3894.436,3135.703
|
||||||
3615.381,3411.222
|
3615.381,3411.222
|
||||||
3807.51,3525.049
|
3807.51,3525.049
|
||||||
3197.936,3515.207
|
3197.936,3515.207
|
||||||
3817.654,3505.676
|
3817.654,3505.676
|
||||||
3604.482,3749.862
|
3604.482,3749.862
|
||||||
4054.217,3389.18
|
4054.217,3389.18
|
||||||
4064.973,3110.13
|
4064.973,3110.13
|
||||||
3828.174,3994.395
|
3828.174,3994.395
|
||||||
3464.949,3706.928
|
3464.949,3706.928
|
||||||
3458.833,3818.998
|
3458.833,3818.998
|
||||||
3447.594,3354.733
|
3447.594,3354.733
|
||||||
3148.49,2938.606
|
3148.49,2938.606
|
||||||
3403.617,3000.615
|
3403.617,3000.615
|
||||||
3619.143,3712.188
|
3619.143,3712.188
|
||||||
3676.835,3294.72
|
3676.835,3294.72
|
||||||
4020.2,3668.025
|
4020.2,3668.025
|
||||||
3365.03,3288.992
|
3365.03,3288.992
|
||||||
3395.001,3047.487
|
3395.001,3047.487
|
||||||
3444.301,3644.15
|
3444.301,3644.15
|
||||||
3258.341,3412.968
|
3258.341,3412.968
|
||||||
3640.787,3028.915
|
3640.787,3028.915
|
||||||
3523.975,2984.702
|
3523.975,2984.702
|
||||||
3661.891,3124.492
|
3661.891,3124.492
|
||||||
3802.303,3098.351
|
3802.303,3098.351
|
||||||
3774.646,3486.505
|
3774.646,3486.505
|
||||||
3622.705,1967.98
|
3622.705,1967.98
|
||||||
3508.677,2629.166
|
3508.677,2629.166
|
||||||
3566.014,2717.307
|
3566.014,2717.307
|
||||||
3849.619,1697.053
|
3849.619,1697.053
|
||||||
3315.839,1708.413
|
3315.839,1708.413
|
||||||
3423.282,2104.829
|
3423.282,2104.829
|
||||||
3750.536,2822.277
|
3750.536,2822.277
|
||||||
3554.167,2610.241
|
3554.167,2610.241
|
||||||
3826.747,3645.146
|
3826.747,3645.146
|
||||||
3892.643,2795.429
|
3892.643,2795.429
|
||||||
3832.114,2572.367
|
3832.114,2572.367
|
||||||
3497.325,3586.324
|
3497.325,3586.324
|
||||||
3348.139,3108.224
|
3348.139,3108.224
|
||||||
3317.933,2944.826
|
3317.933,2944.826
|
||||||
3605.83,2890.459
|
3605.83,2890.459
|
||||||
3539.072,3132.536
|
3539.072,3132.536
|
||||||
3121.903,3343.355
|
3121.903,3343.355
|
||||||
2942.032,3478.153
|
2942.032,3478.153
|
||||||
3445.076,3762.927
|
3445.076,3762.927
|
||||||
3100.771,3377.621
|
3100.771,3377.621
|
||||||
3189.105,3326.58
|
3189.105,3326.58
|
||||||
3281.825,3443.852
|
3281.825,3443.852
|
||||||
2678.243,3830.363
|
2678.243,3830.363
|
||||||
2955.651,2863.628
|
2955.651,2863.628
|
||||||
2696.034,3640.54
|
2696.034,3640.54
|
||||||
3370.494,3203.94
|
3370.494,3203.94
|
||||||
3300.628,3755.641
|
3300.628,3755.641
|
||||||
3488.021,3931.192
|
3488.021,3931.192
|
||||||
3330.963,2780.609
|
3330.963,2780.609
|
||||||
3154.885,2986.501
|
3154.885,2986.501
|
||||||
3375.716,3359.562
|
3375.716,3359.562
|
||||||
2841.549,3077.406
|
2841.549,3077.406
|
||||||
3404.81,3385.657
|
3404.81,3385.657
|
||||||
3757.787,3352.594
|
3757.787,3352.594
|
||||||
3717.258,3264.236
|
3717.258,3264.236
|
||||||
3353.01,3659.337
|
3353.01,3659.337
|
||||||
3190.808,3732.121
|
3190.808,3732.121
|
||||||
3165.985,3380.969
|
3165.985,3380.969
|
||||||
3797.661,3264.325
|
3797.661,3264.325
|
||||||
3347.68,3711.328
|
3347.68,3711.328
|
||||||
3604.306,3454.656
|
3604.306,3454.656
|
||||||
3615.091,3547.976
|
3615.091,3547.976
|
||||||
3291.287,3115.255
|
3291.287,3115.255
|
||||||
|
|||||||
|
@@ -1,101 +1,101 @@
|
|||||||
No filters,test data
|
No filters,test data
|
||||||
4244.309,3423.133
|
4244.309,3423.133
|
||||||
4172.153,3839.874
|
4172.153,3839.874
|
||||||
4318.167,3651.161
|
4318.167,3651.161
|
||||||
4141.307,3886.542
|
4141.307,3886.542
|
||||||
4153.546,3293.166
|
4153.546,3293.166
|
||||||
4313.574,3639.47
|
4313.574,3639.47
|
||||||
4212.2,3422.614
|
4212.2,3422.614
|
||||||
3944.194,3928.898
|
3944.194,3928.898
|
||||||
3470.867,3395.562
|
3470.867,3395.562
|
||||||
3680.557,4233.545
|
3680.557,4233.545
|
||||||
3639.904,3739.869
|
3639.904,3739.869
|
||||||
3601.206,4331.278
|
3601.206,4331.278
|
||||||
3602.268,3561.573
|
3602.268,3561.573
|
||||||
4041.709,3360.442
|
4041.709,3360.442
|
||||||
3326.243,3898.576
|
3326.243,3898.576
|
||||||
3519.295,3710.73
|
3519.295,3710.73
|
||||||
3421.704,3785.601
|
3421.704,3785.601
|
||||||
3761.544,3720.579
|
3761.544,3720.579
|
||||||
3849.834,3419.051
|
3849.834,3419.051
|
||||||
3771.48,3525.297
|
3771.48,3525.297
|
||||||
3477.096,3709.462
|
3477.096,3709.462
|
||||||
3752.154,3410.653
|
3752.154,3410.653
|
||||||
3828.539,3784.068
|
3828.539,3784.068
|
||||||
3601.283,4371.022
|
3601.283,4371.022
|
||||||
3550.535,3353.485
|
3550.535,3353.485
|
||||||
3573.931,4326.953
|
3573.931,4326.953
|
||||||
3989.022,3630.239
|
3989.022,3630.239
|
||||||
3758.771,3187.932
|
3758.771,3187.932
|
||||||
3764.081,3348.153
|
3764.081,3348.153
|
||||||
3552.11,3210.788
|
3552.11,3210.788
|
||||||
3624.703,3580.683
|
3624.703,3580.683
|
||||||
3495.138,3702.232
|
3495.138,3702.232
|
||||||
3679.786,3211.763
|
3679.786,3211.763
|
||||||
3965.941,4386.728
|
3965.941,4386.728
|
||||||
3481.692,4312.93
|
3481.692,4312.93
|
||||||
3472.266,3638.52
|
3472.266,3638.52
|
||||||
3902.087,4356.89
|
3902.087,4356.89
|
||||||
4162.868,3770.82
|
4162.868,3770.82
|
||||||
3556.674,3899.06
|
3556.674,3899.06
|
||||||
3568.287,3768.694
|
3568.287,3768.694
|
||||||
3813.52,3794.494
|
3813.52,3794.494
|
||||||
3538.6,4233.813
|
3538.6,4233.813
|
||||||
3583.165,3598.301
|
3583.165,3598.301
|
||||||
3545.668,3574.602
|
3545.668,3574.602
|
||||||
3498.538,3731.551
|
3498.538,3731.551
|
||||||
4069.232,3732.176
|
4069.232,3732.176
|
||||||
3488.875,4390.112
|
3488.875,4390.112
|
||||||
3471.224,4308.19
|
3471.224,4308.19
|
||||||
3487.893,3713.36
|
3487.893,3713.36
|
||||||
3556.706,3783.748
|
3556.706,3783.748
|
||||||
4134.049,4075.267
|
4134.049,4075.267
|
||||||
3619.571,3616.779
|
3619.571,3616.779
|
||||||
3880.411,4017.523
|
3880.411,4017.523
|
||||||
3437.287,4024.127
|
3437.287,4024.127
|
||||||
3571.923,4136.496
|
3571.923,4136.496
|
||||||
3355.569,4297.359
|
3355.569,4297.359
|
||||||
3621.019,3428.405
|
3621.019,3428.405
|
||||||
3432.623,3962.733
|
3432.623,3962.733
|
||||||
3541.66,3558.748
|
3541.66,3558.748
|
||||||
3506.787,3874.117
|
3506.787,3874.117
|
||||||
4124.636,3616.127
|
4124.636,3616.127
|
||||||
3585.123,3360.593
|
3585.123,3360.593
|
||||||
3572.09,3416.381
|
3572.09,3416.381
|
||||||
3344.338,3861.743
|
3344.338,3861.743
|
||||||
3540.41,3412.915
|
3540.41,3412.915
|
||||||
3768.322,3490.888
|
3768.322,3490.888
|
||||||
3865.742,3149.312
|
3865.742,3149.312
|
||||||
3543.772,3438.211
|
3543.772,3438.211
|
||||||
3649.759,3538.124
|
3649.759,3538.124
|
||||||
3714.508,3298.845
|
3714.508,3298.845
|
||||||
3989.119,3652.572
|
3989.119,3652.572
|
||||||
4004.341,3688.486
|
4004.341,3688.486
|
||||||
3942.733,3533.375
|
3942.733,3533.375
|
||||||
3767.707,3692.636
|
3767.707,3692.636
|
||||||
3854.87,3567.363
|
3854.87,3567.363
|
||||||
3818.102,4325.471
|
3818.102,4325.471
|
||||||
4326.545,3464.113
|
4326.545,3464.113
|
||||||
3331.279,3346.4
|
3331.279,3346.4
|
||||||
3782.928,3599.129
|
3782.928,3599.129
|
||||||
3441.486,3571.214
|
3441.486,3571.214
|
||||||
3688.115,3778.354
|
3688.115,3778.354
|
||||||
3523.493,4268.157
|
3523.493,4268.157
|
||||||
3350.288,3241.872
|
3350.288,3241.872
|
||||||
3337.668,3405.69
|
3337.668,3405.69
|
||||||
3467.795,3655.209
|
3467.795,3655.209
|
||||||
3695.322,3161.427
|
3695.322,3161.427
|
||||||
4111.114,3289.313
|
4111.114,3289.313
|
||||||
3499.726,3157.723
|
3499.726,3157.723
|
||||||
3731.525,3334.048
|
3731.525,3334.048
|
||||||
4226.314,3315.567
|
4226.314,3315.567
|
||||||
3430.903,3176.271
|
3430.903,3176.271
|
||||||
3480.629,3296.73
|
3480.629,3296.73
|
||||||
3930.84,3302.929
|
3930.84,3302.929
|
||||||
3702.883,3251.164
|
3702.883,3251.164
|
||||||
3839.087,3180.461
|
3839.087,3180.461
|
||||||
3831.296,3215.8
|
3831.296,3215.8
|
||||||
3615.657,3262.533
|
3615.657,3262.533
|
||||||
3766.269,3446.736
|
3766.269,3446.736
|
||||||
3556.331,4274.897
|
3556.331,4274.897
|
||||||
3843.934,3370.384
|
3843.934,3370.384
|
||||||
|
|||||||
|
@@ -8,7 +8,7 @@ ERROR=0
|
|||||||
|
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r requirements.txt
|
||||||
|
|
||||||
until curl --output /dev/null --silent --fail http://localhost:4444/api/status; do
|
until curl --output /dev/null --silent --fail http://127.0.0.1:4444/api/status; do
|
||||||
printf '.'
|
printf '.'
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -1,9 +1,46 @@
|
|||||||
|
import queue
|
||||||
from multiprocessing import Process, Queue
|
from multiprocessing import Process, Queue
|
||||||
import socket
|
import socket
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
def _start_tcp_server(port, server_queue: Queue, ipv6, verbose):
|
||||||
|
sock = socket.socket(
|
||||||
|
socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_STREAM
|
||||||
|
)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.bind(("::1" if ipv6 else "127.0.0.1", port))
|
||||||
|
sock.listen(8)
|
||||||
|
while True:
|
||||||
|
connection, address = sock.accept()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
buf = connection.recv(4096)
|
||||||
|
if buf == b"":
|
||||||
|
break
|
||||||
|
|
||||||
|
reply = buf # Default to echo
|
||||||
|
try:
|
||||||
|
# See if there is a custom reply, but don't block
|
||||||
|
custom_reply = server_queue.get(block=False)
|
||||||
|
reply = custom_reply
|
||||||
|
except queue.Empty:
|
||||||
|
pass # No custom reply, just echo
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print("SERVER: ", reply)
|
||||||
|
connection.sendall(reply)
|
||||||
|
except (ConnectionResetError, BrokenPipeError):
|
||||||
|
break # Client closed connection
|
||||||
|
except Exception:
|
||||||
|
if verbose:
|
||||||
|
traceback.print_exc()
|
||||||
|
break # Exit on other errors
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
class TcpServer:
|
class TcpServer:
|
||||||
def __init__(self,port,ipv6,proxy_port=None, verbose=False):
|
def __init__(self, port, ipv6, proxy_port=None, verbose=False):
|
||||||
self.proxy_port = proxy_port
|
self.proxy_port = proxy_port
|
||||||
self.ipv6 = ipv6
|
self.ipv6 = ipv6
|
||||||
self.port = port
|
self.port = port
|
||||||
@@ -12,43 +49,30 @@ class TcpServer:
|
|||||||
self._regen_process()
|
self._regen_process()
|
||||||
|
|
||||||
def _regen_process(self):
|
def _regen_process(self):
|
||||||
def _startServer(port, server_queue:Queue):
|
self.server = Process(
|
||||||
sock = socket.socket(socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_STREAM)
|
target=_start_tcp_server,
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
args=[self.port, self._server_data_queue, self.ipv6, self.verbose],
|
||||||
sock.bind(('::1' if self.ipv6 else '127.0.0.1', port))
|
)
|
||||||
sock.listen(8)
|
|
||||||
while True:
|
|
||||||
connection,address = sock.accept()
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
buf = connection.recv(4096)
|
|
||||||
if buf == b'':
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
buf = server_queue.get(block=False)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if self.verbose:
|
|
||||||
print("SERVER: ", buf)
|
|
||||||
connection.sendall(buf)
|
|
||||||
except Exception:
|
|
||||||
if self.verbose:
|
|
||||||
traceback.print_exc()
|
|
||||||
connection.close()
|
|
||||||
self.server = Process(target=_startServer,args=[self.port, self._server_data_queue])
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.server.start()
|
self.server.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.server.terminate()
|
self.server.terminate()
|
||||||
self.server.join()
|
self.server.join()
|
||||||
self._regen_process()
|
self._regen_process()
|
||||||
|
|
||||||
def connect_client(self):
|
def connect_client(self):
|
||||||
self.client_sock = socket.socket(socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_STREAM)
|
self.client_sock = socket.socket(
|
||||||
|
socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_STREAM
|
||||||
|
)
|
||||||
self.client_sock.settimeout(1)
|
self.client_sock.settimeout(1)
|
||||||
self.client_sock.connect(('::1' if self.ipv6 else '127.0.0.1', self.proxy_port if self.proxy_port else self.port))
|
self.client_sock.connect(
|
||||||
|
(
|
||||||
|
"::1" if self.ipv6 else "127.0.0.1",
|
||||||
|
self.proxy_port if self.proxy_port else self.port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def close_client(self):
|
def close_client(self):
|
||||||
if self.client_sock:
|
if self.client_sock:
|
||||||
@@ -60,7 +84,7 @@ class TcpServer:
|
|||||||
if server_reply:
|
if server_reply:
|
||||||
self._server_data_queue.put(server_reply)
|
self._server_data_queue.put(server_reply)
|
||||||
self.client_sock.sendall(packet)
|
self.client_sock.sendall(packet)
|
||||||
|
|
||||||
def recv_packet(self):
|
def recv_packet(self):
|
||||||
try:
|
try:
|
||||||
return self.client_sock.recv(4096)
|
return self.client_sock.recv(4096)
|
||||||
@@ -68,7 +92,7 @@ class TcpServer:
|
|||||||
if self.verbose:
|
if self.verbose:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def sendCheckData(self, data, get_data=False):
|
def sendCheckData(self, data, get_data=False):
|
||||||
self.connect_client()
|
self.connect_client()
|
||||||
self.send_packet(data)
|
self.send_packet(data)
|
||||||
|
|||||||
@@ -1,35 +1,94 @@
|
|||||||
from multiprocessing import Process
|
from multiprocessing import Process, Queue
|
||||||
import socket
|
import socket
|
||||||
|
import queue
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
def _start_udp_server(port, server_queue: Queue, ipv6, verbose):
|
||||||
|
sock = socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
sock.bind(("::1" if ipv6 else "127.0.0.1", port))
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
bytesAddressPair = sock.recvfrom(4096)
|
||||||
|
message = bytesAddressPair[0]
|
||||||
|
address = bytesAddressPair[1]
|
||||||
|
|
||||||
|
reply = message # Default to echo
|
||||||
|
try:
|
||||||
|
# See if there is a custom reply, but don't block
|
||||||
|
custom_reply = server_queue.get(block=False)
|
||||||
|
reply = custom_reply
|
||||||
|
except queue.Empty:
|
||||||
|
pass # No custom reply, just echo
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"SERVER: sending {reply} to {address}")
|
||||||
|
sock.sendto(reply, address)
|
||||||
|
except Exception:
|
||||||
|
if verbose:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
class UdpServer:
|
class UdpServer:
|
||||||
def __init__(self,port,ipv6, proxy_port = None):
|
def __init__(self, port, ipv6, proxy_port=None, verbose=False):
|
||||||
def _startServer(port):
|
|
||||||
sock = socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_DGRAM)
|
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
||||||
sock.bind(('::1' if ipv6 else '127.0.0.1', port))
|
|
||||||
while True:
|
|
||||||
bytesAddressPair = sock.recvfrom(432)
|
|
||||||
message = bytesAddressPair[0]
|
|
||||||
address = bytesAddressPair[1]
|
|
||||||
sock.sendto(message, address)
|
|
||||||
|
|
||||||
self.ipv6 = ipv6
|
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self.ipv6 = ipv6
|
||||||
self.proxy_port = proxy_port
|
self.proxy_port = proxy_port
|
||||||
self.server = Process(target=_startServer,args=[port])
|
self.verbose = verbose
|
||||||
|
self._server_data_queue = Queue()
|
||||||
|
self._regen_process()
|
||||||
|
|
||||||
|
def _regen_process(self):
|
||||||
|
self.server = Process(
|
||||||
|
target=_start_udp_server,
|
||||||
|
args=[self.port, self._server_data_queue, self.ipv6, self.verbose],
|
||||||
|
)
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.server.start()
|
self.server.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.server.terminate()
|
self.server.terminate()
|
||||||
|
self.server.join()
|
||||||
|
self._regen_process()
|
||||||
|
|
||||||
def sendCheckData(self,data):
|
def connect_client(self):
|
||||||
s = socket.socket(socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_DGRAM)
|
self.client_sock = socket.socket(
|
||||||
s.settimeout(2)
|
socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_DGRAM
|
||||||
s.sendto(data, ('::1' if self.ipv6 else '127.0.0.1', self.proxy_port if self.proxy_port else self.port))
|
)
|
||||||
|
self.client_sock.settimeout(1)
|
||||||
|
self.client_sock.connect(
|
||||||
|
(
|
||||||
|
"::1" if self.ipv6 else "127.0.0.1",
|
||||||
|
self.proxy_port if self.proxy_port else self.port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def close_client(self):
|
||||||
|
if self.client_sock:
|
||||||
|
self.client_sock.close()
|
||||||
|
|
||||||
|
def send_packet(self, packet, server_reply=None):
|
||||||
|
if self.verbose:
|
||||||
|
print("CLIENT: ", packet)
|
||||||
|
if server_reply:
|
||||||
|
self._server_data_queue.put(server_reply)
|
||||||
|
self.client_sock.sendall(packet)
|
||||||
|
|
||||||
|
def recv_packet(self):
|
||||||
try:
|
try:
|
||||||
received_data = s.recvfrom(432)
|
return self.client_sock.recv(4096)
|
||||||
except Exception:
|
except (TimeoutError, ConnectionResetError):
|
||||||
|
if self.verbose:
|
||||||
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
return received_data[0] == data
|
|
||||||
|
def sendCheckData(self, data, get_data=False):
|
||||||
|
self.connect_client()
|
||||||
|
self.send_packet(data)
|
||||||
|
received_data = self.recv_packet()
|
||||||
|
self.close_client()
|
||||||
|
if get_data:
|
||||||
|
return received_data
|
||||||
|
return received_data == data
|
||||||
|
|||||||
Reference in New Issue
Block a user