dsa
This commit is contained in:
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 }}
|
||||||
|
|||||||
101
Dockerfile
101
Dockerfile
@@ -1,49 +1,52 @@
|
|||||||
|
|
||||||
# Firegex Dockerfile UUID signature
|
# Firegex Dockerfile UUID signature
|
||||||
# cf1795af-3284-4183-a888-81ad3590ad84
|
# cf1795af-3284-4183-a888-81ad3590ad84
|
||||||
# Needed for run.py to detect the Dockerfile
|
# Needed for run.py to detect the Dockerfile
|
||||||
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM oven/bun AS frontend
|
FROM --platform=$BUILDPLATFORM oven/bun AS frontend
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ADD ./frontend/package.json .
|
ADD ./frontend/package.json .
|
||||||
ADD ./frontend/bun.lock .
|
ADD ./frontend/bun.lock .
|
||||||
RUN bun i
|
RUN bun i
|
||||||
COPY ./frontend/ .
|
COPY ./frontend/ .
|
||||||
RUN bun run build
|
RUN bun run build
|
||||||
|
|
||||||
# Base fedora container
|
# Base Ubuntu container
|
||||||
FROM --platform=$TARGETARCH quay.io/fedora/fedora:43 AS base
|
FROM --platform=$TARGETARCH ubuntu:24.04 AS base
|
||||||
RUN dnf -y update && dnf install -y python3.14 libnetfilter_queue \
|
RUN apt-get update && apt-get install -y python3 libnetfilter-queue1 \
|
||||||
libnfnetlink libmnl libcap-ng-utils nftables \
|
libnfnetlink0 libmnl0 libcap-ng-utils nftables \
|
||||||
vectorscan libtins python3-nftables libpcap && dnf clean all
|
libhs5 libtins4.4 python3-nftables libpcap0.8 && \
|
||||||
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
RUN mkdir -p /execute/modules
|
|
||||||
WORKDIR /execute
|
RUN mkdir -p /execute/modules
|
||||||
|
WORKDIR /execute
|
||||||
FROM --platform=$TARGETARCH base AS compiler
|
|
||||||
|
FROM --platform=$TARGETARCH base AS compiler
|
||||||
RUN dnf -y update && dnf install -y python3.14-devel @development-tools gcc-c++ \
|
|
||||||
libnetfilter_queue-devel libnfnetlink-devel libmnl-devel \
|
RUN apt-get update && apt-get install -y python3-dev build-essential g++ \
|
||||||
vectorscan-devel libtins-devel libpcap-devel boost-devel
|
libnetfilter-queue-dev libnfnetlink-dev libmnl-dev \
|
||||||
|
libhyperscan-dev libtins-dev libpcap-dev libboost-dev pkg-config && \
|
||||||
COPY ./backend/binsrc /execute/binsrc
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
|
|
||||||
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.14 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
|
COPY ./backend/binsrc /execute/binsrc
|
||||||
|
RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
|
||||||
#Building main conteiner
|
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.12 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
|
||||||
FROM --platform=$TARGETARCH base AS final
|
|
||||||
|
#Building main conteiner
|
||||||
COPY ./backend/requirements.txt /execute/requirements.txt
|
FROM --platform=$TARGETARCH base AS final
|
||||||
COPY ./fgex-lib /execute/fgex-lib
|
|
||||||
|
COPY ./backend/requirements.txt /execute/requirements.txt
|
||||||
RUN dnf -y update && dnf install -y gcc-c++ python3.14-devel uv git &&\
|
COPY ./fgex-lib /execute/fgex-lib
|
||||||
uv pip install --no-cache --system ./fgex-lib &&\
|
|
||||||
uv pip install --no-cache --system -r /execute/requirements.txt &&\
|
RUN apt-get update && apt-get install -y g++ python3-dev python3-pip git && \
|
||||||
uv cache clean && dnf remove -y gcc-c++ python3.14-devel uv git && dnf clean all
|
pip3 install --no-cache-dir --break-system-packages ./fgex-lib && \
|
||||||
|
pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt && \
|
||||||
COPY ./backend/ /execute/
|
apt-get remove -y g++ python3-dev git && \
|
||||||
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
|
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
COPY --from=frontend /app/dist/ ./frontend/
|
|
||||||
|
COPY ./backend/ /execute/
|
||||||
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"]
|
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
|
||||||
|
COPY --from=frontend /app/dist/ ./frontend/
|
||||||
|
|
||||||
|
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import asyncio
|
|||||||
import traceback
|
import traceback
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
import time
|
import time
|
||||||
|
import json
|
||||||
from utils import run_func
|
from utils import run_func
|
||||||
from utils import DEBUG
|
from utils import DEBUG
|
||||||
from utils import nicenessify
|
from utils import nicenessify
|
||||||
@@ -35,11 +36,12 @@ class FiregexInterceptor:
|
|||||||
self.last_time_exception = 0
|
self.last_time_exception = 0
|
||||||
self.outstrem_function = None
|
self.outstrem_function = None
|
||||||
self.expection_function = None
|
self.expection_function = None
|
||||||
|
self.traffic_function = None
|
||||||
self.outstrem_task: asyncio.Task
|
self.outstrem_task: asyncio.Task
|
||||||
self.outstrem_buffer = ""
|
self.outstrem_buffer = ""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def start(cls, srv: Service, outstream_func=None, exception_func=None):
|
async def start(cls, srv: Service, outstream_func=None, exception_func=None, traffic_func=None):
|
||||||
self = cls()
|
self = cls()
|
||||||
self.srv = srv
|
self.srv = srv
|
||||||
self.filter_map_lock = asyncio.Lock()
|
self.filter_map_lock = asyncio.Lock()
|
||||||
@@ -47,6 +49,7 @@ class FiregexInterceptor:
|
|||||||
self.sock_conn_lock = asyncio.Lock()
|
self.sock_conn_lock = asyncio.Lock()
|
||||||
self.outstrem_function = outstream_func
|
self.outstrem_function = outstream_func
|
||||||
self.expection_function = exception_func
|
self.expection_function = exception_func
|
||||||
|
self.traffic_function = traffic_func
|
||||||
if not self.sock_conn_lock.locked():
|
if not self.sock_conn_lock.locked():
|
||||||
await self.sock_conn_lock.acquire()
|
await self.sock_conn_lock.acquire()
|
||||||
self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock"
|
self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock"
|
||||||
@@ -83,6 +86,16 @@ class FiregexInterceptor:
|
|||||||
self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n"
|
self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n"
|
||||||
if self.outstrem_function:
|
if self.outstrem_function:
|
||||||
await run_func(self.outstrem_function, self.srv.id, out_data)
|
await run_func(self.outstrem_function, self.srv.id, out_data)
|
||||||
|
# Parse JSON traffic events (if binary emits them)
|
||||||
|
if self.traffic_function:
|
||||||
|
for line in out_data.splitlines():
|
||||||
|
if line.startswith("{"): # JSON event from binary
|
||||||
|
try:
|
||||||
|
event = json.loads(line)
|
||||||
|
if "ts" in event and "verdict" in event: # Basic validation
|
||||||
|
await run_func(self.traffic_function, self.srv.id, event)
|
||||||
|
except (json.JSONDecodeError, KeyError):
|
||||||
|
pass # Ignore malformed JSON, keep backward compat with raw logs
|
||||||
|
|
||||||
async def _start_binary(self):
|
async def _start_binary(self):
|
||||||
proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))
|
proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
fastapi[all]
|
fastapi[all]
|
||||||
httpx
|
httpx
|
||||||
uvicorn[standard]
|
uvicorn[standard]
|
||||||
psutil
|
psutil
|
||||||
python-jose[cryptography]
|
python-jose[cryptography]
|
||||||
python-socketio
|
python-socketio
|
||||||
brotli
|
brotli
|
||||||
#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py
|
#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ async def startup():
|
|||||||
utils.socketio.on("nfproxy-outstream-leave", leave_outstream)
|
utils.socketio.on("nfproxy-outstream-leave", leave_outstream)
|
||||||
utils.socketio.on("nfproxy-exception-join", join_exception)
|
utils.socketio.on("nfproxy-exception-join", join_exception)
|
||||||
utils.socketio.on("nfproxy-exception-leave", leave_exception)
|
utils.socketio.on("nfproxy-exception-leave", leave_exception)
|
||||||
|
utils.socketio.on("nfproxy-traffic-join", join_traffic)
|
||||||
|
utils.socketio.on("nfproxy-traffic-leave", leave_traffic)
|
||||||
|
|
||||||
async def shutdown():
|
async def shutdown():
|
||||||
db.backup()
|
db.backup()
|
||||||
@@ -133,7 +135,10 @@ async def outstream_func(service_id, data):
|
|||||||
async def exception_func(service_id, timestamp):
|
async def exception_func(service_id, timestamp):
|
||||||
await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}")
|
await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}")
|
||||||
|
|
||||||
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func)
|
async def traffic_func(service_id, event):
|
||||||
|
await utils.socketio.emit(f"nfproxy-traffic-{service_id}", event, room=f"nfproxy-traffic-{service_id}")
|
||||||
|
|
||||||
|
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func, traffic_func=traffic_func)
|
||||||
|
|
||||||
@app.get('/services', response_model=list[ServiceModel])
|
@app.get('/services', response_model=list[ServiceModel])
|
||||||
async def get_service_list():
|
async def get_service_list():
|
||||||
@@ -368,6 +373,28 @@ async def get_pyfilters_code(service_id: str):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@app.get('/services/{service_id}/traffic')
|
||||||
|
async def get_traffic_events(service_id: str, limit: int = 500):
|
||||||
|
"""Get recent traffic events from the service ring buffer"""
|
||||||
|
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
|
||||||
|
raise HTTPException(status_code=400, detail="This service does not exists!")
|
||||||
|
try:
|
||||||
|
events = firewall.get(service_id).get_traffic_events(limit)
|
||||||
|
return {"events": events, "count": len(events)}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.post('/services/{service_id}/traffic/clear', response_model=StatusMessageModel)
|
||||||
|
async def clear_traffic_events(service_id: str):
|
||||||
|
"""Clear traffic event history for a service"""
|
||||||
|
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
|
||||||
|
raise HTTPException(status_code=400, detail="This service does not exists!")
|
||||||
|
try:
|
||||||
|
firewall.get(service_id).clear_traffic_events()
|
||||||
|
return {"status": "ok"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
#Socket io events
|
#Socket io events
|
||||||
async def join_outstream(sid, data):
|
async def join_outstream(sid, data):
|
||||||
"""Client joins a room."""
|
"""Client joins a room."""
|
||||||
@@ -397,3 +424,20 @@ async def leave_exception(sid, data):
|
|||||||
if srv:
|
if srv:
|
||||||
await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}")
|
await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}")
|
||||||
|
|
||||||
|
async def join_traffic(sid, data):
|
||||||
|
"""Client joins traffic viewer room and gets initial event history."""
|
||||||
|
srv = data.get("service")
|
||||||
|
if srv:
|
||||||
|
room = f"nfproxy-traffic-{srv}"
|
||||||
|
await utils.socketio.enter_room(sid, room)
|
||||||
|
try:
|
||||||
|
events = firewall.get(srv).get_traffic_events(500)
|
||||||
|
await utils.socketio.emit("nfproxy-traffic-history", {"events": events}, room=sid)
|
||||||
|
except Exception:
|
||||||
|
pass # Service may not exist or not started
|
||||||
|
|
||||||
|
async def leave_traffic(sid, data):
|
||||||
|
"""Client leaves traffic viewer room."""
|
||||||
|
srv = data.get("service")
|
||||||
|
if srv:
|
||||||
|
await utils.socketio.leave_room(sid, f"nfproxy-traffic-{srv}")
|
||||||
|
|||||||
@@ -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. 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
|
||||||
@@ -13,6 +13,8 @@ import { Firewall } from './pages/Firewall';
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import NFProxy from './pages/NFProxy';
|
import NFProxy from './pages/NFProxy';
|
||||||
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
||||||
|
import TrafficViewer from './pages/NFProxy/TrafficViewer';
|
||||||
|
import TrafficViewerMain from './pages/TrafficViewer';
|
||||||
import { useAuthStore } from './js/store';
|
import { useAuthStore } from './js/store';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -172,7 +174,9 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
|
|||||||
</Route>
|
</Route>
|
||||||
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
|
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
|
||||||
<Route path=":srv" element={<ServiceDetailsNFProxy />} />
|
<Route path=":srv" element={<ServiceDetailsNFProxy />} />
|
||||||
|
<Route path=":srv/traffic" element={<TrafficViewer />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="traffic" element={<TrafficViewerMain />} />
|
||||||
<Route path="firewall" element={<Firewall />} />
|
<Route path="firewall" element={<Firewall />} />
|
||||||
<Route path="porthijack" element={<PortHijack />} />
|
<Route path="porthijack" element={<PortHijack />} />
|
||||||
<Route path="*" element={<HomeRedirector />} />
|
<Route path="*" element={<HomeRedirector />} />
|
||||||
|
|||||||
@@ -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,139 @@
|
|||||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||||
import { ImCross } from "react-icons/im"
|
import { ImCross } from "react-icons/im"
|
||||||
import { nfproxy, Service } from './utils';
|
import { nfproxy, Service } from './utils';
|
||||||
import PortAndInterface from '../PortAndInterface';
|
import PortAndInterface from '../PortAndInterface';
|
||||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||||
|
|
||||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||||
|
|
||||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
name: "",
|
name: "",
|
||||||
port:edit?.port??8080,
|
port:edit?.port??8080,
|
||||||
ip_int:edit?.ip_int??"",
|
ip_int:edit?.ip_int??"",
|
||||||
proto:edit?.proto??"tcp",
|
proto:edit?.proto??"tcp",
|
||||||
fail_open: edit?.fail_open??false,
|
fail_open: edit?.fail_open??false,
|
||||||
autostart: true
|
autostart: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: initialValues,
|
initialValues: initialValues,
|
||||||
validate:{
|
validate:{
|
||||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||||
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
|
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
|
||||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (opened){
|
if (opened){
|
||||||
form.setInitialValues(initialValues)
|
form.setInitialValues(initialValues)
|
||||||
form.reset()
|
form.reset()
|
||||||
}
|
}
|
||||||
}, [opened])
|
}, [opened])
|
||||||
|
|
||||||
const close = () =>{
|
const close = () =>{
|
||||||
onClose()
|
onClose()
|
||||||
form.reset()
|
form.reset()
|
||||||
setError(null)
|
setError(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [submitLoading, setSubmitLoading] = useState(false)
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
const [error, setError] = useState<string|null>(null)
|
const [error, setError] = useState<string|null>(null)
|
||||||
|
|
||||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||||
setSubmitLoading(true)
|
setSubmitLoading(true)
|
||||||
if (edit){
|
if (edit){
|
||||||
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
||||||
if (!res){
|
if (!res){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
}else{
|
}else{
|
||||||
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||||
if (res.status === "ok" && res.service_id){
|
if (res.status === "ok" && res.service_id){
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
close();
|
close();
|
||||||
if (autostart) nfproxy.servicestart(res.service_id)
|
if (autostart) nfproxy.servicestart(res.service_id)
|
||||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||||
}else{
|
}else{
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Invalid request! [ "+res.status+" ]")
|
setError("Invalid request! [ "+res.status+" ]")
|
||||||
}
|
}
|
||||||
}).catch( err => {
|
}).catch( err => {
|
||||||
setSubmitLoading(false)
|
setSubmitLoading(false)
|
||||||
setError("Request Failed! [ "+err+" ]")
|
setError("Request Failed! [ "+err+" ]")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||||
{!edit?<TextInput
|
{!edit?<TextInput
|
||||||
label="Service name"
|
label="Service name"
|
||||||
placeholder="Challenge 01"
|
placeholder="Challenge 01"
|
||||||
{...form.getInputProps('name')}
|
{...form.getInputProps('name')}
|
||||||
/>:null}
|
/>:null}
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
|
|
||||||
<Box className='center-flex'>
|
<Box className='center-flex'>
|
||||||
<Box>
|
<Box>
|
||||||
{!edit?<Switch
|
{!edit?<Switch
|
||||||
label="Auto-Start Service"
|
label="Auto-Start Service"
|
||||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||||
/>:null}
|
/>:null}
|
||||||
<Space h="sm" />
|
<Space h="sm" />
|
||||||
<Switch
|
<Switch
|
||||||
label={<Box className='center-flex'>
|
label={<Box className='center-flex'>
|
||||||
Enable fail-open nfqueue
|
Enable fail-open nfqueue
|
||||||
<Space w="xs" />
|
<Space w="xs" />
|
||||||
<Tooltip label={<>
|
<Tooltip label={<>
|
||||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||||
</>}>
|
</>}>
|
||||||
<IoMdInformationCircleOutline size={15} />
|
<IoMdInformationCircleOutline size={15} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>}
|
</Box>}
|
||||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="flex-spacer"></Box>
|
<Box className="flex-spacer"></Box>
|
||||||
{edit?null:<SegmentedControl
|
{edit?null:<SegmentedControl
|
||||||
data={[
|
data={[
|
||||||
{ label: 'TCP', value: 'tcp' },
|
{ label: 'TCP', value: 'tcp' },
|
||||||
{ label: 'HTTP', value: 'http' },
|
{ label: 'HTTP', value: 'http' },
|
||||||
]}
|
]}
|
||||||
{...form.getInputProps('proto')}
|
{...form.getInputProps('proto')}
|
||||||
/>}
|
/>}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group justify='flex-end' mt="md" mb="sm">
|
<Group justify='flex-end' mt="md" mb="sm">
|
||||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{error?<>
|
{error?<>
|
||||||
<Space h="md" />
|
<Space h="md" />
|
||||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||||
Error: {error}
|
Error: {error}
|
||||||
</Notification><Space h="md" />
|
</Notification><Space h="md" />
|
||||||
</>:null}
|
</>:null}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AddEditService;
|
export default AddEditService;
|
||||||
|
|||||||
@@ -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,6 +7,7 @@ import { PiWallLight } from "react-icons/pi";
|
|||||||
import { useNavbarStore } from "../../js/store";
|
import { useNavbarStore } from "../../js/store";
|
||||||
import { getMainPath } from "../../js/utils";
|
import { getMainPath } from "../../js/utils";
|
||||||
import { BsRegex } from "react-icons/bs";
|
import { BsRegex } from "react-icons/bs";
|
||||||
|
import { MdVisibility } from "react-icons/md";
|
||||||
|
|
||||||
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
||||||
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
|
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
|
||||||
@@ -40,6 +41,7 @@ export default function NavBar() {
|
|||||||
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
|
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
|
||||||
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
|
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
|
||||||
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
||||||
|
<NavBarButton navigate="traffic" closeNav={closeNav} name="Traffic Viewer" color="cyan" icon={<MdVisibility size={19} />} />
|
||||||
{/* <Box px="xs" mt="lg">
|
{/* <Box px="xs" mt="lg">
|
||||||
<Title order={5}>Experimental Features 🧪</Title>
|
<Title order={5}>Experimental Features 🧪</Title>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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("")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|||||||
239
frontend/src/pages/NFProxy/TrafficViewer.tsx
Normal file
239
frontend/src/pages/NFProxy/TrafficViewer.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import { ActionIcon, Badge, Box, Code, Divider, Grid, LoadingOverlay, Modal, ScrollArea, Space, Table, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||||
|
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||||
|
import { errorNotify, isMediumScreen, socketio } from '../../js/utils';
|
||||||
|
import { FaArrowLeft, FaFilter, FaTrash } from 'react-icons/fa';
|
||||||
|
import { MdDoubleArrow } from "react-icons/md";
|
||||||
|
import { useListState } from '@mantine/hooks';
|
||||||
|
|
||||||
|
type TrafficEvent = {
|
||||||
|
ts: number;
|
||||||
|
direction?: string;
|
||||||
|
src_ip?: string;
|
||||||
|
src_port?: number;
|
||||||
|
dst_ip?: string;
|
||||||
|
dst_port?: number;
|
||||||
|
proto?: string;
|
||||||
|
size?: number;
|
||||||
|
verdict?: string;
|
||||||
|
filter?: string;
|
||||||
|
sample_hex?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TrafficViewer() {
|
||||||
|
const { srv } = useParams();
|
||||||
|
const services = nfproxyServiceQuery();
|
||||||
|
const serviceInfo = services.data?.find((s: any) => s.service_id === srv);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const isMedium = isMediumScreen();
|
||||||
|
const [events, eventsHandlers] = useListState<TrafficEvent>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [filterText, setFilterText] = useState('');
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState<TrafficEvent | null>(null);
|
||||||
|
const [modalOpened, setModalOpened] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!srv) return;
|
||||||
|
|
||||||
|
// Fetch historical events
|
||||||
|
const fetchHistory = async () => {
|
||||||
|
try {
|
||||||
|
const response = await nfproxy.gettraffic(srv, 500);
|
||||||
|
if (response.events) {
|
||||||
|
eventsHandlers.setState(response.events);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch traffic history:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchHistory();
|
||||||
|
|
||||||
|
// Join Socket.IO room
|
||||||
|
socketio.emit("nfproxy-traffic-join", { service: srv });
|
||||||
|
|
||||||
|
// Listen for historical events on initial join
|
||||||
|
socketio.on("nfproxy-traffic-history", (data: { events: TrafficEvent[] }) => {
|
||||||
|
if (data.events && data.events.length > 0) {
|
||||||
|
eventsHandlers.setState(data.events);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for live events
|
||||||
|
socketio.on(`nfproxy-traffic-${srv}`, (event: TrafficEvent) => {
|
||||||
|
eventsHandlers.append(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socketio.emit("nfproxy-traffic-leave", { service: srv });
|
||||||
|
socketio.off(`nfproxy-traffic-${srv}`);
|
||||||
|
socketio.off("nfproxy-traffic-history");
|
||||||
|
};
|
||||||
|
}, [srv]);
|
||||||
|
|
||||||
|
if (services.isLoading) return <LoadingOverlay visible={true} />;
|
||||||
|
if (!srv || !serviceInfo) return <Navigate to="/" replace />;
|
||||||
|
|
||||||
|
const clearEvents = async () => {
|
||||||
|
try {
|
||||||
|
await nfproxy.cleartraffic(srv);
|
||||||
|
eventsHandlers.setState([]);
|
||||||
|
} catch (err) {
|
||||||
|
errorNotify("Failed to clear traffic events", String(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredEvents = events.filter((e: TrafficEvent) => {
|
||||||
|
if (!filterText) return true;
|
||||||
|
const search = filterText.toLowerCase();
|
||||||
|
return (
|
||||||
|
e.src_ip?.toLowerCase().includes(search) ||
|
||||||
|
e.dst_ip?.toLowerCase().includes(search) ||
|
||||||
|
e.verdict?.toLowerCase().includes(search) ||
|
||||||
|
e.filter?.toLowerCase().includes(search) ||
|
||||||
|
e.proto?.toLowerCase().includes(search)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatTimestamp = (ts: number) => {
|
||||||
|
const date = new Date(ts);
|
||||||
|
return date.toLocaleTimeString() + '.' + date.getMilliseconds().toString().padStart(3, '0');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVerdictColor = (verdict?: string) => {
|
||||||
|
switch (verdict?.toLowerCase()) {
|
||||||
|
case 'accept': return 'teal';
|
||||||
|
case 'drop': return 'red';
|
||||||
|
case 'reject': return 'orange';
|
||||||
|
case 'edited': return 'yellow';
|
||||||
|
default: return 'gray';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDetails = (event: TrafficEvent) => {
|
||||||
|
setSelectedEvent(event);
|
||||||
|
setModalOpened(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<LoadingOverlay visible={loading} />
|
||||||
|
<Box className={isMedium ? 'center-flex' : 'center-flex-row'} style={{ justifyContent: "space-between" }} px="md" mt="lg">
|
||||||
|
<Title order={1}>
|
||||||
|
<Box className="center-flex">
|
||||||
|
<MdDoubleArrow /><Space w="sm" />Traffic Viewer - {serviceInfo.name}
|
||||||
|
</Box>
|
||||||
|
</Title>
|
||||||
|
<Box className='center-flex'>
|
||||||
|
<Badge color="cyan" radius="md" size="xl" variant="filled" mr="sm">
|
||||||
|
{filteredEvents.length} events
|
||||||
|
</Badge>
|
||||||
|
<Tooltip label="Clear events" color="red">
|
||||||
|
<ActionIcon color="red" size="lg" radius="md" onClick={clearEvents} variant="filled">
|
||||||
|
<FaTrash size="18px" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Space w="md" />
|
||||||
|
<Tooltip label="Go back" color="cyan">
|
||||||
|
<ActionIcon color="cyan" onClick={() => navigate(-1)} size="lg" radius="md" variant="filled">
|
||||||
|
<FaArrowLeft size="20px" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider my="md" />
|
||||||
|
|
||||||
|
<Box px="md">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Filter by IP, verdict, filter name, or protocol..."
|
||||||
|
value={filterText}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
|
||||||
|
leftSection={<FaFilter />}
|
||||||
|
mb="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollArea style={{ height: 'calc(100vh - 280px)' }}>
|
||||||
|
<Table striped highlightOnHover>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Time</Table.Th>
|
||||||
|
<Table.Th>Direction</Table.Th>
|
||||||
|
<Table.Th>Source</Table.Th>
|
||||||
|
<Table.Th>Destination</Table.Th>
|
||||||
|
<Table.Th>Protocol</Table.Th>
|
||||||
|
<Table.Th>Size</Table.Th>
|
||||||
|
<Table.Th>Filter</Table.Th>
|
||||||
|
<Table.Th>Verdict</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{filteredEvents.length === 0 ? (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={8} style={{ textAlign: 'center', padding: '2rem' }}>
|
||||||
|
<Text c="dimmed">
|
||||||
|
{filterText ? 'No events match your filter' : 'No traffic events yet. Waiting for traffic...'}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
) : (
|
||||||
|
filteredEvents.slice(-500).reverse().map((event: TrafficEvent, idx: number) => (
|
||||||
|
<Table.Tr key={idx} onClick={() => openDetails(event)} style={{ cursor: 'pointer' }}>
|
||||||
|
<Table.Td><Code>{formatTimestamp(event.ts)}</Code></Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge size="sm" variant="dot" color={event.direction === 'in' ? 'blue' : 'grape'}>
|
||||||
|
{event.direction || 'unknown'}
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{event.src_ip || '-'}:{event.src_port || '-'}</Table.Td>
|
||||||
|
<Table.Td>{event.dst_ip || '-'}:{event.dst_port || '-'}</Table.Td>
|
||||||
|
<Table.Td><Badge size="sm" color="violet">{event.proto || 'unknown'}</Badge></Table.Td>
|
||||||
|
<Table.Td>{event.size ? `${event.size} B` : '-'}</Table.Td>
|
||||||
|
<Table.Td><Code>{event.filter || '-'}</Code></Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge color={getVerdictColor(event.verdict)} size="sm">
|
||||||
|
{event.verdict || 'unknown'}
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</ScrollArea>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Payload details modal */}
|
||||||
|
<Modal
|
||||||
|
opened={modalOpened}
|
||||||
|
onClose={() => setModalOpened(false)}
|
||||||
|
title="Event Details"
|
||||||
|
size="xl"
|
||||||
|
>
|
||||||
|
{selectedEvent && (
|
||||||
|
<Box>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}><strong>Timestamp:</strong> {formatTimestamp(selectedEvent.ts)}</Grid.Col>
|
||||||
|
<Grid.Col span={6}><strong>Direction:</strong> {selectedEvent.direction || 'unknown'}</Grid.Col>
|
||||||
|
<Grid.Col span={6}><strong>Source:</strong> {selectedEvent.src_ip}:{selectedEvent.src_port}</Grid.Col>
|
||||||
|
<Grid.Col span={6}><strong>Destination:</strong> {selectedEvent.dst_ip}:{selectedEvent.dst_port}</Grid.Col>
|
||||||
|
<Grid.Col span={6}><strong>Protocol:</strong> {selectedEvent.proto || 'unknown'}</Grid.Col>
|
||||||
|
<Grid.Col span={6}><strong>Size:</strong> {selectedEvent.size ? `${selectedEvent.size} B` : '-'}</Grid.Col>
|
||||||
|
<Grid.Col span={6}><strong>Filter:</strong> {selectedEvent.filter || '-'}</Grid.Col>
|
||||||
|
<Grid.Col span={6}><strong>Verdict:</strong> {selectedEvent.verdict || 'unknown'}</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
{selectedEvent.sample_hex && (
|
||||||
|
<>
|
||||||
|
<Divider my="md" label="Payload Sample (Hex)" />
|
||||||
|
<ScrollArea style={{ maxHeight: '300px' }}>
|
||||||
|
<Code block>{selectedEvent.sample_hex}</Code>
|
||||||
|
</ScrollArea>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
138
frontend/src/pages/TrafficViewer/index.tsx
Normal file
138
frontend/src/pages/TrafficViewer/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import { ActionIcon, Badge, Box, Card, Divider, Group, LoadingOverlay, Space, Text, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||||
|
import { isMediumScreen } from '../../js/utils';
|
||||||
|
import { MdDoubleArrow, MdVisibility } from 'react-icons/md';
|
||||||
|
import { TbPlugConnected } from 'react-icons/tb';
|
||||||
|
import { FaServer } from 'react-icons/fa';
|
||||||
|
|
||||||
|
export default function TrafficViewer() {
|
||||||
|
const services = nfproxyServiceQuery();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const isMedium = isMediumScreen();
|
||||||
|
|
||||||
|
if (services.isLoading) return <LoadingOverlay visible={true} />;
|
||||||
|
|
||||||
|
const activeServices = services.data?.filter(s => s.status === 'active') || [];
|
||||||
|
const stoppedServices = services.data?.filter(s => s.status !== 'active') || [];
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Box px="md" mt="lg">
|
||||||
|
<Title order={1} className="center-flex">
|
||||||
|
<ThemeIcon radius="md" size="lg" variant='filled' color='cyan'>
|
||||||
|
<MdVisibility size={24} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Space w="sm" />
|
||||||
|
Traffic Viewer
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" mt="sm">
|
||||||
|
Monitor live network traffic for all NFProxy services
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider my="lg" />
|
||||||
|
|
||||||
|
{services.data?.length === 0 ? (
|
||||||
|
<Box px="md">
|
||||||
|
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
|
||||||
|
No NFProxy services found
|
||||||
|
</Title>
|
||||||
|
<Space h="xs" />
|
||||||
|
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
|
||||||
|
Create a service in the Netfilter Proxy section to start monitoring traffic
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box px="md">
|
||||||
|
{activeServices.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Title order={3} mb="md">
|
||||||
|
<Badge color="teal" size="lg" mr="xs">Active</Badge>
|
||||||
|
Running Services
|
||||||
|
</Title>
|
||||||
|
{activeServices.map(service => (
|
||||||
|
<Card key={service.service_id} shadow="sm" padding="lg" radius="md" withBorder mb="md">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Box>
|
||||||
|
<Group>
|
||||||
|
<ThemeIcon color="lime" variant="light" size="lg">
|
||||||
|
<TbPlugConnected size={20} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<div>
|
||||||
|
<Text fw={700} size="lg">{service.name}</Text>
|
||||||
|
<Group gap="xs" mt={4}>
|
||||||
|
<Badge color="cyan" size="sm">:{service.port}</Badge>
|
||||||
|
<Badge color="violet" size="sm">{service.proto}</Badge>
|
||||||
|
<Badge color="gray" size="sm">{service.ip_int}</Badge>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Group>
|
||||||
|
<Box style={{ textAlign: 'right' }}>
|
||||||
|
<Badge color="orange" size="sm" mb={4}>
|
||||||
|
{service.edited_packets} edited
|
||||||
|
</Badge>
|
||||||
|
<br />
|
||||||
|
<Badge color="yellow" size="sm">
|
||||||
|
{service.blocked_packets} blocked
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
<Tooltip label="View traffic">
|
||||||
|
<ActionIcon
|
||||||
|
color="cyan"
|
||||||
|
size="xl"
|
||||||
|
radius="md"
|
||||||
|
variant="filled"
|
||||||
|
onClick={() => navigate(`/nfproxy/${service.service_id}/traffic`)}
|
||||||
|
>
|
||||||
|
<MdDoubleArrow size="24px" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<Space h="xl" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{stoppedServices.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Title order={3} mb="md">
|
||||||
|
<Badge color="red" size="lg" mr="xs">Stopped</Badge>
|
||||||
|
Inactive Services
|
||||||
|
</Title>
|
||||||
|
{stoppedServices.map(service => (
|
||||||
|
<Card key={service.service_id} shadow="sm" padding="lg" radius="md" withBorder mb="md" opacity={0.6}>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Box>
|
||||||
|
<Group>
|
||||||
|
<ThemeIcon color="gray" variant="light" size="lg">
|
||||||
|
<FaServer size={18} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<div>
|
||||||
|
<Text fw={500} size="lg" c="dimmed">{service.name}</Text>
|
||||||
|
<Group gap="xs" mt={4}>
|
||||||
|
<Badge color="gray" size="sm">:{service.port}</Badge>
|
||||||
|
<Badge color="gray" size="sm">{service.proto}</Badge>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Start service to view traffic
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
|
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": false,
|
"esModuleInterop": false,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
run.py
2
run.py
@@ -268,7 +268,7 @@ def write_compose(skip_password = True):
|
|||||||
"firewall": {
|
"firewall": {
|
||||||
"restart": "unless-stopped",
|
"restart": "unless-stopped",
|
||||||
"container_name": "firegex",
|
"container_name": "firegex",
|
||||||
"build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}",
|
"build" if g.build else "image": "." if g.build else f"ghcr.io/ilyastar9999/firegex:{args.version}",
|
||||||
"network_mode": "host",
|
"network_mode": "host",
|
||||||
"environment": [
|
"environment": [
|
||||||
f"PORT={args.port}",
|
f"PORT={args.port}",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
Reference in New Issue
Block a user