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
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
docker_build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
run_tests: true
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
run_tests: true
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and run firegex
|
||||
if: matrix.run_tests
|
||||
run: python3 run.py start -P testpassword
|
||||
|
||||
- name: Run tests
|
||||
if: matrix.run_tests
|
||||
run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update version in setup.py
|
||||
run: >-
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" backend/utils/__init__.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }}
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
docker_manifest:
|
||||
needs: docker_build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and push multi-platform manifest
|
||||
run: |
|
||||
# Create manifest list for specific tag
|
||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64
|
||||
|
||||
# Annotate the manifest with architecture info
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}
|
||||
|
||||
# Create manifest list for latest tag
|
||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64
|
||||
|
||||
# Annotate the latest manifest with architecture info
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest
|
||||
|
||||
create-rootfs-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [docker_manifest]
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get latest release tag
|
||||
id: get_tag
|
||||
run: |
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')
|
||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Latest release tag: $LATEST_TAG"
|
||||
|
||||
- name: Export rootfs for amd64
|
||||
run: |
|
||||
echo "Creating and exporting amd64 container..."
|
||||
CONTAINER_ID=$(docker create --platform linux/amd64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing amd64 rootfs..."
|
||||
gzip firegex-rootfs-amd64.tar
|
||||
ls -lh firegex-rootfs-amd64.tar.gz
|
||||
|
||||
- name: Export rootfs for arm64
|
||||
run: |
|
||||
echo "Creating and exporting arm64 container..."
|
||||
CONTAINER_ID=$(docker create --platform linux/arm64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing arm64 rootfs..."
|
||||
gzip firegex-rootfs-arm64.tar
|
||||
ls -lh firegex-rootfs-arm64.tar.gz
|
||||
|
||||
- name: Upload rootfs assets to release
|
||||
run: |
|
||||
echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..."
|
||||
gh release upload ${{ steps.get_tag.outputs.tag }} \
|
||||
firegex-rootfs-amd64.tar.gz \
|
||||
firegex-rootfs-arm64.tar.gz \
|
||||
--clobber
|
||||
echo "Assets uploaded successfully!"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: Create and publish Docker images
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
docker_build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
run_tests: true
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
run_tests: true
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and run firegex
|
||||
if: matrix.run_tests
|
||||
run: python3 run.py start -P testpassword
|
||||
|
||||
- name: Run tests
|
||||
if: matrix.run_tests
|
||||
run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update version in setup.py
|
||||
run: >-
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" backend/utils/__init__.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }}
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
docker_manifest:
|
||||
needs: docker_build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and push multi-platform manifest
|
||||
run: |
|
||||
# Create manifest list for specific tag
|
||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64
|
||||
|
||||
# Annotate the manifest with architecture info
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}
|
||||
|
||||
# Create manifest list for latest tag
|
||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64
|
||||
|
||||
# Annotate the latest manifest with architecture info
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest
|
||||
|
||||
create-rootfs-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [docker_manifest]
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get latest release tag
|
||||
id: get_tag
|
||||
run: |
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')
|
||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Latest release tag: $LATEST_TAG"
|
||||
|
||||
- name: Export rootfs for amd64
|
||||
run: |
|
||||
echo "Creating and exporting amd64 container..."
|
||||
CONTAINER_ID=$(docker create --platform linux/amd64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing amd64 rootfs..."
|
||||
gzip firegex-rootfs-amd64.tar
|
||||
ls -lh firegex-rootfs-amd64.tar.gz
|
||||
|
||||
- name: Export rootfs for arm64
|
||||
run: |
|
||||
echo "Creating and exporting arm64 container..."
|
||||
CONTAINER_ID=$(docker create --platform linux/arm64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing arm64 rootfs..."
|
||||
gzip firegex-rootfs-arm64.tar
|
||||
ls -lh firegex-rootfs-arm64.tar.gz
|
||||
|
||||
- name: Upload rootfs assets to release
|
||||
run: |
|
||||
echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..."
|
||||
gh release upload ${{ steps.get_tag.outputs.tag }} \
|
||||
firegex-rootfs-amd64.tar.gz \
|
||||
firegex-rootfs-arm64.tar.gz \
|
||||
--clobber
|
||||
echo "Assets uploaded successfully!"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
92
.github/workflows/pypi-publish-fgex.yml
vendored
92
.github/workflows/pypi-publish-fgex.yml
vendored
@@ -1,46 +1,46 @@
|
||||
# This workflow will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Upload Python Package (fgex alias)
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
- name: Update version in setup.py
|
||||
run: >-
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
||||
- name: Build package
|
||||
run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN_FGEX }}
|
||||
# # This workflow will upload a Python Package using Twine when a release is created
|
||||
# # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# # This workflow uses actions that are not certified by GitHub.
|
||||
# # They are provided by a third-party and are governed by
|
||||
# # separate terms of service, privacy policy, and support
|
||||
# # documentation.
|
||||
|
||||
# name: Upload Python Package (fgex alias)
|
||||
|
||||
# on:
|
||||
# release:
|
||||
# types:
|
||||
# - published
|
||||
|
||||
# permissions:
|
||||
# contents: read
|
||||
|
||||
# jobs:
|
||||
# deploy:
|
||||
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: '3.x'
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip
|
||||
# pip install build
|
||||
# - name: Extract tag name
|
||||
# id: tag
|
||||
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
# - name: Update version in setup.py
|
||||
# run: >-
|
||||
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
||||
# - name: Build package
|
||||
# run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
||||
# - name: Publish package
|
||||
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
# with:
|
||||
# user: __token__
|
||||
# password: ${{ secrets.PYPI_API_TOKEN_FGEX }}
|
||||
|
||||
94
.github/workflows/pypi-publish.yml
vendored
94
.github/workflows/pypi-publish.yml
vendored
@@ -1,47 +1,47 @@
|
||||
# This workflow will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
- name: Update version in setup.py
|
||||
run: >-
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
- name: Build package
|
||||
run: cd fgex-lib && python -m build && mv ./dist ../
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
# # This workflow will upload a Python Package using Twine when a release is created
|
||||
# # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# # This workflow uses actions that are not certified by GitHub.
|
||||
# # They are provided by a third-party and are governed by
|
||||
# # separate terms of service, privacy policy, and support
|
||||
# # documentation.
|
||||
|
||||
# name: Upload Python Package
|
||||
|
||||
# on:
|
||||
# release:
|
||||
# types:
|
||||
# - published
|
||||
|
||||
# permissions:
|
||||
# contents: read
|
||||
|
||||
# jobs:
|
||||
# deploy:
|
||||
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: '3.x'
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip
|
||||
# pip install build
|
||||
# - name: Extract tag name
|
||||
# id: tag
|
||||
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
# - name: Update version in setup.py
|
||||
# run: >-
|
||||
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
# - name: Build package
|
||||
# run: cd fgex-lib && python -m build && mv ./dist ../
|
||||
# - name: Publish package
|
||||
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
# with:
|
||||
# user: __token__
|
||||
# password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
|
||||
101
Dockerfile
101
Dockerfile
@@ -1,49 +1,52 @@
|
||||
|
||||
# Firegex Dockerfile UUID signature
|
||||
# cf1795af-3284-4183-a888-81ad3590ad84
|
||||
# Needed for run.py to detect the Dockerfile
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM oven/bun AS frontend
|
||||
WORKDIR /app
|
||||
ADD ./frontend/package.json .
|
||||
ADD ./frontend/bun.lock .
|
||||
RUN bun i
|
||||
COPY ./frontend/ .
|
||||
RUN bun run build
|
||||
|
||||
# Base fedora container
|
||||
FROM --platform=$TARGETARCH quay.io/fedora/fedora:43 AS base
|
||||
RUN dnf -y update && dnf install -y python3.14 libnetfilter_queue \
|
||||
libnfnetlink libmnl libcap-ng-utils nftables \
|
||||
vectorscan libtins python3-nftables libpcap && dnf clean all
|
||||
|
||||
RUN mkdir -p /execute/modules
|
||||
WORKDIR /execute
|
||||
|
||||
FROM --platform=$TARGETARCH base AS compiler
|
||||
|
||||
RUN dnf -y update && dnf install -y python3.14-devel @development-tools gcc-c++ \
|
||||
libnetfilter_queue-devel libnfnetlink-devel libmnl-devel \
|
||||
vectorscan-devel libtins-devel libpcap-devel boost-devel
|
||||
|
||||
COPY ./backend/binsrc /execute/binsrc
|
||||
RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
|
||||
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.14 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
|
||||
|
||||
#Building main conteiner
|
||||
FROM --platform=$TARGETARCH base AS final
|
||||
|
||||
COPY ./backend/requirements.txt /execute/requirements.txt
|
||||
COPY ./fgex-lib /execute/fgex-lib
|
||||
|
||||
RUN dnf -y update && dnf install -y gcc-c++ python3.14-devel uv git &&\
|
||||
uv pip install --no-cache --system ./fgex-lib &&\
|
||||
uv pip install --no-cache --system -r /execute/requirements.txt &&\
|
||||
uv cache clean && dnf remove -y gcc-c++ python3.14-devel uv git && dnf clean all
|
||||
|
||||
COPY ./backend/ /execute/
|
||||
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
|
||||
COPY --from=frontend /app/dist/ ./frontend/
|
||||
|
||||
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"]
|
||||
|
||||
# Firegex Dockerfile UUID signature
|
||||
# cf1795af-3284-4183-a888-81ad3590ad84
|
||||
# Needed for run.py to detect the Dockerfile
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM oven/bun AS frontend
|
||||
WORKDIR /app
|
||||
ADD ./frontend/package.json .
|
||||
ADD ./frontend/bun.lock .
|
||||
RUN bun i
|
||||
COPY ./frontend/ .
|
||||
RUN bun run build
|
||||
|
||||
# Base Ubuntu container
|
||||
FROM --platform=$TARGETARCH ubuntu:24.04 AS base
|
||||
RUN apt-get update && apt-get install -y python3 libnetfilter-queue1 \
|
||||
libnfnetlink0 libmnl0 libcap-ng-utils nftables \
|
||||
libhs5 libtins4.4 python3-nftables libpcap0.8 && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /execute/modules
|
||||
WORKDIR /execute
|
||||
|
||||
FROM --platform=$TARGETARCH base AS compiler
|
||||
|
||||
RUN apt-get update && apt-get install -y python3-dev build-essential g++ \
|
||||
libnetfilter-queue-dev libnfnetlink-dev libmnl-dev \
|
||||
libhyperscan-dev libtins-dev libpcap-dev libboost-dev pkg-config && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY ./backend/binsrc /execute/binsrc
|
||||
RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
|
||||
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.12 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
|
||||
|
||||
#Building main conteiner
|
||||
FROM --platform=$TARGETARCH base AS final
|
||||
|
||||
COPY ./backend/requirements.txt /execute/requirements.txt
|
||||
COPY ./fgex-lib /execute/fgex-lib
|
||||
|
||||
RUN apt-get update && apt-get install -y g++ python3-dev python3-pip git && \
|
||||
pip3 install --no-cache-dir --break-system-packages ./fgex-lib && \
|
||||
pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt && \
|
||||
apt-get remove -y g++ python3-dev git && \
|
||||
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY ./backend/ /execute/
|
||||
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
|
||||
COPY --from=frontend /app/dist/ ./frontend/
|
||||
|
||||
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"]
|
||||
|
||||
@@ -5,6 +5,7 @@ import asyncio
|
||||
import traceback
|
||||
from fastapi import HTTPException
|
||||
import time
|
||||
import json
|
||||
from utils import run_func
|
||||
from utils import DEBUG
|
||||
from utils import nicenessify
|
||||
@@ -35,11 +36,12 @@ class FiregexInterceptor:
|
||||
self.last_time_exception = 0
|
||||
self.outstrem_function = None
|
||||
self.expection_function = None
|
||||
self.traffic_function = None
|
||||
self.outstrem_task: asyncio.Task
|
||||
self.outstrem_buffer = ""
|
||||
|
||||
@classmethod
|
||||
async def start(cls, srv: Service, outstream_func=None, exception_func=None):
|
||||
async def start(cls, srv: Service, outstream_func=None, exception_func=None, traffic_func=None):
|
||||
self = cls()
|
||||
self.srv = srv
|
||||
self.filter_map_lock = asyncio.Lock()
|
||||
@@ -47,6 +49,7 @@ class FiregexInterceptor:
|
||||
self.sock_conn_lock = asyncio.Lock()
|
||||
self.outstrem_function = outstream_func
|
||||
self.expection_function = exception_func
|
||||
self.traffic_function = traffic_func
|
||||
if not self.sock_conn_lock.locked():
|
||||
await self.sock_conn_lock.acquire()
|
||||
self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock"
|
||||
@@ -83,6 +86,16 @@ class FiregexInterceptor:
|
||||
self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n"
|
||||
if self.outstrem_function:
|
||||
await run_func(self.outstrem_function, self.srv.id, out_data)
|
||||
# Parse JSON traffic events (if binary emits them)
|
||||
if self.traffic_function:
|
||||
for line in out_data.splitlines():
|
||||
if line.startswith("{"): # JSON event from binary
|
||||
try:
|
||||
event = json.loads(line)
|
||||
if "ts" in event and "verdict" in event: # Basic validation
|
||||
await run_func(self.traffic_function, self.srv.id, event)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
pass # Ignore malformed JSON, keep backward compat with raw logs
|
||||
|
||||
async def _start_binary(self):
|
||||
proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from modules.nfproxy.firegex import FiregexInterceptor
|
||||
from modules.nfproxy.nftables import FiregexTables, FiregexFilter
|
||||
from modules.nfproxy.models import Service, PyFilter
|
||||
@@ -12,7 +13,7 @@ class STATUS:
|
||||
nft = FiregexTables()
|
||||
|
||||
class ServiceManager:
|
||||
def __init__(self, srv: Service, db, outstream_func=None, exception_func=None):
|
||||
def __init__(self, srv: Service, db, outstream_func=None, exception_func=None, traffic_func=None):
|
||||
self.srv = srv
|
||||
self.db = db
|
||||
self.status = STATUS.STOP
|
||||
@@ -21,11 +22,17 @@ class ServiceManager:
|
||||
self.interceptor = None
|
||||
self.outstream_function = outstream_func
|
||||
self.last_exception_time = 0
|
||||
self.traffic_events = deque(maxlen=500) # Ring buffer for traffic viewer
|
||||
async def excep_internal_handler(srv, exc_time):
|
||||
self.last_exception_time = exc_time
|
||||
if exception_func:
|
||||
await run_func(exception_func, srv, exc_time)
|
||||
self.exception_function = excep_internal_handler
|
||||
async def traffic_internal_handler(srv, event):
|
||||
self.traffic_events.append(event)
|
||||
if traffic_func:
|
||||
await run_func(traffic_func, srv, event)
|
||||
self.traffic_function = traffic_internal_handler
|
||||
|
||||
async def _update_filters_from_db(self):
|
||||
pyfilters = [
|
||||
@@ -69,7 +76,7 @@ class ServiceManager:
|
||||
async def start(self):
|
||||
if not self.interceptor:
|
||||
nft.delete(self.srv)
|
||||
self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
||||
self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function, traffic_func=self.traffic_function)
|
||||
await self._update_filters_from_db()
|
||||
self._set_status(STATUS.ACTIVE)
|
||||
|
||||
@@ -87,14 +94,24 @@ class ServiceManager:
|
||||
async def update_filters(self):
|
||||
async with self.lock:
|
||||
await self._update_filters_from_db()
|
||||
|
||||
def get_traffic_events(self, limit: int = 500):
|
||||
"""Return recent traffic events from ring buffer"""
|
||||
events_list = list(self.traffic_events)
|
||||
return events_list[-limit:] if limit < len(events_list) else events_list
|
||||
|
||||
def clear_traffic_events(self):
|
||||
"""Clear traffic event history"""
|
||||
self.traffic_events.clear()
|
||||
|
||||
class FirewallManager:
|
||||
def __init__(self, db:SQLite, outstream_func=None, exception_func=None):
|
||||
def __init__(self, db:SQLite, outstream_func=None, exception_func=None, traffic_func=None):
|
||||
self.db = db
|
||||
self.service_table: dict[str, ServiceManager] = {}
|
||||
self.lock = asyncio.Lock()
|
||||
self.outstream_function = outstream_func
|
||||
self.exception_function = exception_func
|
||||
self.traffic_function = traffic_func
|
||||
|
||||
async def close(self):
|
||||
for key in list(self.service_table.keys()):
|
||||
@@ -116,7 +133,7 @@ class FirewallManager:
|
||||
srv = Service.from_dict(srv)
|
||||
if srv.id in self.service_table:
|
||||
continue
|
||||
self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
||||
self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function, traffic_func=self.traffic_function)
|
||||
await self.service_table[srv.id].next(srv.status)
|
||||
|
||||
def get(self,srv_id) -> ServiceManager:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
fastapi[all]
|
||||
httpx
|
||||
uvicorn[standard]
|
||||
psutil
|
||||
python-jose[cryptography]
|
||||
python-socketio
|
||||
brotli
|
||||
#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py
|
||||
fastapi[all]
|
||||
httpx
|
||||
uvicorn[standard]
|
||||
psutil
|
||||
python-jose[cryptography]
|
||||
python-socketio
|
||||
brotli
|
||||
#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-exception-join", join_exception)
|
||||
utils.socketio.on("nfproxy-exception-leave", leave_exception)
|
||||
utils.socketio.on("nfproxy-traffic-join", join_traffic)
|
||||
utils.socketio.on("nfproxy-traffic-leave", leave_traffic)
|
||||
|
||||
async def shutdown():
|
||||
db.backup()
|
||||
@@ -133,7 +135,10 @@ async def outstream_func(service_id, data):
|
||||
async def exception_func(service_id, timestamp):
|
||||
await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}")
|
||||
|
||||
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func)
|
||||
async def traffic_func(service_id, event):
|
||||
await utils.socketio.emit(f"nfproxy-traffic-{service_id}", event, room=f"nfproxy-traffic-{service_id}")
|
||||
|
||||
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func, traffic_func=traffic_func)
|
||||
|
||||
@app.get('/services', response_model=list[ServiceModel])
|
||||
async def get_service_list():
|
||||
@@ -368,6 +373,28 @@ async def get_pyfilters_code(service_id: str):
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
|
||||
@app.get('/services/{service_id}/traffic')
|
||||
async def get_traffic_events(service_id: str, limit: int = 500):
|
||||
"""Get recent traffic events from the service ring buffer"""
|
||||
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
|
||||
raise HTTPException(status_code=400, detail="This service does not exists!")
|
||||
try:
|
||||
events = firewall.get(service_id).get_traffic_events(limit)
|
||||
return {"events": events, "count": len(events)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post('/services/{service_id}/traffic/clear', response_model=StatusMessageModel)
|
||||
async def clear_traffic_events(service_id: str):
|
||||
"""Clear traffic event history for a service"""
|
||||
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
|
||||
raise HTTPException(status_code=400, detail="This service does not exists!")
|
||||
try:
|
||||
firewall.get(service_id).clear_traffic_events()
|
||||
return {"status": "ok"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
#Socket io events
|
||||
async def join_outstream(sid, data):
|
||||
"""Client joins a room."""
|
||||
@@ -397,3 +424,20 @@ async def leave_exception(sid, data):
|
||||
if srv:
|
||||
await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}")
|
||||
|
||||
async def join_traffic(sid, data):
|
||||
"""Client joins traffic viewer room and gets initial event history."""
|
||||
srv = data.get("service")
|
||||
if srv:
|
||||
room = f"nfproxy-traffic-{srv}"
|
||||
await utils.socketio.enter_room(sid, room)
|
||||
try:
|
||||
events = firewall.get(srv).get_traffic_events(500)
|
||||
await utils.socketio.emit("nfproxy-traffic-history", {"events": events}, room=sid)
|
||||
except Exception:
|
||||
pass # Service may not exist or not started
|
||||
|
||||
async def leave_traffic(sid, data):
|
||||
"""Client leaves traffic viewer room."""
|
||||
srv = data.get("service")
|
||||
if srv:
|
||||
await utils.socketio.leave_room(sid, f"nfproxy-traffic-{srv}")
|
||||
|
||||
@@ -1,221 +1,221 @@
|
||||
import asyncio
|
||||
from ipaddress import ip_address, ip_interface
|
||||
import os
|
||||
import socket
|
||||
import psutil
|
||||
import sys
|
||||
import nftables
|
||||
from socketio import AsyncServer
|
||||
from fastapi import Path
|
||||
from typing import Annotated
|
||||
from functools import wraps
|
||||
from pydantic import BaseModel, ValidationError
|
||||
import traceback
|
||||
from utils.models import StatusMessageModel
|
||||
from typing import List
|
||||
|
||||
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
|
||||
|
||||
socketio:AsyncServer = None
|
||||
sid_list:set = set()
|
||||
|
||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
|
||||
ON_DOCKER = "DOCKER" in sys.argv
|
||||
DEBUG = "DEBUG" in sys.argv
|
||||
NORELOAD = "NORELOAD" in sys.argv
|
||||
FIREGEX_PORT = int(os.getenv("PORT","4444"))
|
||||
FIREGEX_HOST = os.getenv("HOST","0.0.0.0")
|
||||
FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None)
|
||||
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
|
||||
|
||||
PortType = Annotated[int, Path(gt=0, lt=65536)]
|
||||
|
||||
async def run_func(func, *args, **kwargs):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
async def socketio_emit(elements:list[str]):
|
||||
await socketio.emit("update",elements)
|
||||
|
||||
def refactor_name(name:str):
|
||||
name = name.strip()
|
||||
while " " in name:
|
||||
name = name.replace(" "," ")
|
||||
return name
|
||||
|
||||
class SysctlManager:
|
||||
def __init__(self, ctl_table):
|
||||
self.old_table = {}
|
||||
self.new_table = {}
|
||||
if os.path.isdir("/sys_host/"):
|
||||
self.old_table = dict()
|
||||
self.new_table = dict(ctl_table)
|
||||
for name in ctl_table.keys():
|
||||
self.old_table[name] = read_sysctl(name)
|
||||
|
||||
def write_table(self, table) -> bool:
|
||||
for name, value in table.items():
|
||||
if read_sysctl(name) != value:
|
||||
write_sysctl(name, value)
|
||||
|
||||
def set(self):
|
||||
self.write_table(self.new_table)
|
||||
|
||||
def reset(self):
|
||||
self.write_table(self.old_table)
|
||||
|
||||
def read_sysctl(name:str):
|
||||
with open(f"/sys_host/{name}", "rt") as f:
|
||||
return "1" in f.read()
|
||||
|
||||
def write_sysctl(name:str, value:bool):
|
||||
with open(f"/sys_host/{name}", "wt") as f:
|
||||
f.write("1" if value else "0")
|
||||
|
||||
def list_files(mypath):
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
return [f for f in listdir(mypath) if isfile(join(mypath, f))]
|
||||
|
||||
def ip_parse(ip:str):
|
||||
return str(ip_interface(ip).network)
|
||||
|
||||
def is_ip_parse(ip:str):
|
||||
try:
|
||||
ip_parse(ip)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def addr_parse(ip:str):
|
||||
return str(ip_address(ip))
|
||||
|
||||
def ip_family(ip:str):
|
||||
return "ip6" if ip_interface(ip).version == 6 else "ip"
|
||||
|
||||
def get_interfaces():
|
||||
def _get_interfaces():
|
||||
for int_name, interfs in psutil.net_if_addrs().items():
|
||||
for interf in interfs:
|
||||
if interf.family in [socket.AF_INET, socket.AF_INET6]:
|
||||
yield {"name": int_name, "addr":interf.address}
|
||||
return list(_get_interfaces())
|
||||
|
||||
def nftables_int_to_json(ip_int):
|
||||
ip_int = ip_parse(ip_int)
|
||||
ip_addr = str(ip_int).split("/")[0]
|
||||
ip_addr_cidr = int(str(ip_int).split("/")[1])
|
||||
return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}
|
||||
|
||||
def nftables_json_to_int(ip_json_int):
|
||||
if isinstance(ip_json_int,str):
|
||||
return str(ip_parse(ip_json_int))
|
||||
else:
|
||||
return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}'
|
||||
|
||||
class Singleton(object):
|
||||
__instance = None
|
||||
def __new__(class_, *args, **kwargs):
|
||||
if not isinstance(class_.__instance, class_):
|
||||
class_.__instance = object.__new__(class_, *args, **kwargs)
|
||||
return class_.__instance
|
||||
|
||||
class NFTableManager(Singleton):
|
||||
|
||||
table_name = "firegex"
|
||||
|
||||
def __init__(self, init_cmd, reset_cmd):
|
||||
self.__init_cmds = init_cmd
|
||||
self.__reset_cmds = reset_cmd
|
||||
self.nft = nftables.Nftables()
|
||||
|
||||
def raw_cmd(self, *cmds):
|
||||
return self.nft.json_cmd({"nftables": list(cmds)})
|
||||
|
||||
def cmd(self, *cmds):
|
||||
code, out, err = self.raw_cmd(*cmds)
|
||||
if code == 0:
|
||||
return out
|
||||
else:
|
||||
raise Exception(err)
|
||||
|
||||
def init(self):
|
||||
self.reset()
|
||||
self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}})
|
||||
self.cmd(*self.__init_cmds)
|
||||
|
||||
def reset(self):
|
||||
self.raw_cmd(*self.__reset_cmds)
|
||||
|
||||
def list_rules(self, tables = None, chains = None):
|
||||
for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]:
|
||||
if tables and filter["table"] not in tables:
|
||||
continue
|
||||
if chains and filter["chain"] not in chains:
|
||||
continue
|
||||
yield filter
|
||||
|
||||
def raw_list(self):
|
||||
return self.cmd({"list": {"ruleset": None}})["nftables"]
|
||||
|
||||
def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"):
|
||||
res = obj.model_dump(mode=mode, exclude_unset=not unset)
|
||||
if convert_keys:
|
||||
for from_k, to_k in convert_keys.items():
|
||||
if from_k in res:
|
||||
res[to_k] = res.pop(from_k)
|
||||
if exclude:
|
||||
for ele in exclude:
|
||||
if ele in res:
|
||||
del res[ele]
|
||||
return res
|
||||
|
||||
def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict:
|
||||
if isinstance(obj, list):
|
||||
return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj]
|
||||
return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode)
|
||||
|
||||
def register_event(sio_server: AsyncServer, event_name: str, model: BaseModel, response_model: BaseModel|None = None):
|
||||
def decorator(func):
|
||||
@sio_server.on(event_name) # Automatically registers the event
|
||||
@wraps(func)
|
||||
async def wrapper(sid, data):
|
||||
try:
|
||||
# Parse and validate incoming data
|
||||
parsed_data = model.model_validate(data)
|
||||
except ValidationError:
|
||||
return json_like(StatusMessageModel(status=f"Invalid {event_name} request"))
|
||||
|
||||
# Call the original function with the parsed data
|
||||
result = await func(sid, parsed_data)
|
||||
# If a response model is provided, validate the output
|
||||
if response_model:
|
||||
try:
|
||||
parsed_result = response_model.model_validate(result)
|
||||
except ValidationError:
|
||||
traceback.print_exc()
|
||||
return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response"))
|
||||
else:
|
||||
parsed_result = result
|
||||
# Emit the validated result
|
||||
if parsed_result:
|
||||
if isinstance(parsed_result, BaseModel):
|
||||
return json_like(parsed_result)
|
||||
return parsed_result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def nicenessify(priority:int, pid:int|None=None):
|
||||
try:
|
||||
pid = os.getpid() if pid is None else pid
|
||||
ps = psutil.Process(pid)
|
||||
if os.name == 'posix':
|
||||
ps.nice(priority)
|
||||
except Exception as e:
|
||||
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
||||
pass
|
||||
import asyncio
|
||||
from ipaddress import ip_address, ip_interface
|
||||
import os
|
||||
import socket
|
||||
import psutil
|
||||
import sys
|
||||
import nftables
|
||||
from socketio import AsyncServer
|
||||
from fastapi import Path
|
||||
from typing import Annotated
|
||||
from functools import wraps
|
||||
from pydantic import BaseModel, ValidationError
|
||||
import traceback
|
||||
from utils.models import StatusMessageModel
|
||||
from typing import List
|
||||
|
||||
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
|
||||
|
||||
socketio:AsyncServer = None
|
||||
sid_list:set = set()
|
||||
|
||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
|
||||
ON_DOCKER = "DOCKER" in sys.argv
|
||||
DEBUG = "DEBUG" in sys.argv
|
||||
NORELOAD = "NORELOAD" in sys.argv
|
||||
FIREGEX_PORT = int(os.getenv("PORT","4444"))
|
||||
FIREGEX_HOST = os.getenv("HOST","0.0.0.0")
|
||||
FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None)
|
||||
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
|
||||
|
||||
PortType = Annotated[int, Path(gt=0, lt=65536)]
|
||||
|
||||
async def run_func(func, *args, **kwargs):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
async def socketio_emit(elements:list[str]):
|
||||
await socketio.emit("update",elements)
|
||||
|
||||
def refactor_name(name:str):
|
||||
name = name.strip()
|
||||
while " " in name:
|
||||
name = name.replace(" "," ")
|
||||
return name
|
||||
|
||||
class SysctlManager:
|
||||
def __init__(self, ctl_table):
|
||||
self.old_table = {}
|
||||
self.new_table = {}
|
||||
if os.path.isdir("/sys_host/"):
|
||||
self.old_table = dict()
|
||||
self.new_table = dict(ctl_table)
|
||||
for name in ctl_table.keys():
|
||||
self.old_table[name] = read_sysctl(name)
|
||||
|
||||
def write_table(self, table) -> bool:
|
||||
for name, value in table.items():
|
||||
if read_sysctl(name) != value:
|
||||
write_sysctl(name, value)
|
||||
|
||||
def set(self):
|
||||
self.write_table(self.new_table)
|
||||
|
||||
def reset(self):
|
||||
self.write_table(self.old_table)
|
||||
|
||||
def read_sysctl(name:str):
|
||||
with open(f"/sys_host/{name}", "rt") as f:
|
||||
return "1" in f.read()
|
||||
|
||||
def write_sysctl(name:str, value:bool):
|
||||
with open(f"/sys_host/{name}", "wt") as f:
|
||||
f.write("1" if value else "0")
|
||||
|
||||
def list_files(mypath):
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
return [f for f in listdir(mypath) if isfile(join(mypath, f))]
|
||||
|
||||
def ip_parse(ip:str):
|
||||
return str(ip_interface(ip).network)
|
||||
|
||||
def is_ip_parse(ip:str):
|
||||
try:
|
||||
ip_parse(ip)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def addr_parse(ip:str):
|
||||
return str(ip_address(ip))
|
||||
|
||||
def ip_family(ip:str):
|
||||
return "ip6" if ip_interface(ip).version == 6 else "ip"
|
||||
|
||||
def get_interfaces():
|
||||
def _get_interfaces():
|
||||
for int_name, interfs in psutil.net_if_addrs().items():
|
||||
for interf in interfs:
|
||||
if interf.family in [socket.AF_INET, socket.AF_INET6]:
|
||||
yield {"name": int_name, "addr":interf.address}
|
||||
return list(_get_interfaces())
|
||||
|
||||
def nftables_int_to_json(ip_int):
|
||||
ip_int = ip_parse(ip_int)
|
||||
ip_addr = str(ip_int).split("/")[0]
|
||||
ip_addr_cidr = int(str(ip_int).split("/")[1])
|
||||
return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}
|
||||
|
||||
def nftables_json_to_int(ip_json_int):
|
||||
if isinstance(ip_json_int,str):
|
||||
return str(ip_parse(ip_json_int))
|
||||
else:
|
||||
return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}'
|
||||
|
||||
class Singleton(object):
|
||||
__instance = None
|
||||
def __new__(class_, *args, **kwargs):
|
||||
if not isinstance(class_.__instance, class_):
|
||||
class_.__instance = object.__new__(class_, *args, **kwargs)
|
||||
return class_.__instance
|
||||
|
||||
class NFTableManager(Singleton):
|
||||
|
||||
table_name = "firegex"
|
||||
|
||||
def __init__(self, init_cmd, reset_cmd):
|
||||
self.__init_cmds = init_cmd
|
||||
self.__reset_cmds = reset_cmd
|
||||
self.nft = nftables.Nftables()
|
||||
|
||||
def raw_cmd(self, *cmds):
|
||||
return self.nft.json_cmd({"nftables": list(cmds)})
|
||||
|
||||
def cmd(self, *cmds):
|
||||
code, out, err = self.raw_cmd(*cmds)
|
||||
if code == 0:
|
||||
return out
|
||||
else:
|
||||
raise Exception(err)
|
||||
|
||||
def init(self):
|
||||
self.reset()
|
||||
self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}})
|
||||
self.cmd(*self.__init_cmds)
|
||||
|
||||
def reset(self):
|
||||
self.raw_cmd(*self.__reset_cmds)
|
||||
|
||||
def list_rules(self, tables = None, chains = None):
|
||||
for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]:
|
||||
if tables and filter["table"] not in tables:
|
||||
continue
|
||||
if chains and filter["chain"] not in chains:
|
||||
continue
|
||||
yield filter
|
||||
|
||||
def raw_list(self):
|
||||
return self.cmd({"list": {"ruleset": None}})["nftables"]
|
||||
|
||||
def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"):
|
||||
res = obj.model_dump(mode=mode, exclude_unset=not unset)
|
||||
if convert_keys:
|
||||
for from_k, to_k in convert_keys.items():
|
||||
if from_k in res:
|
||||
res[to_k] = res.pop(from_k)
|
||||
if exclude:
|
||||
for ele in exclude:
|
||||
if ele in res:
|
||||
del res[ele]
|
||||
return res
|
||||
|
||||
def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict:
|
||||
if isinstance(obj, list):
|
||||
return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj]
|
||||
return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode)
|
||||
|
||||
def register_event(sio_server: AsyncServer, event_name: str, model: BaseModel, response_model: BaseModel|None = None):
|
||||
def decorator(func):
|
||||
@sio_server.on(event_name) # Automatically registers the event
|
||||
@wraps(func)
|
||||
async def wrapper(sid, data):
|
||||
try:
|
||||
# Parse and validate incoming data
|
||||
parsed_data = model.model_validate(data)
|
||||
except ValidationError:
|
||||
return json_like(StatusMessageModel(status=f"Invalid {event_name} request"))
|
||||
|
||||
# Call the original function with the parsed data
|
||||
result = await func(sid, parsed_data)
|
||||
# If a response model is provided, validate the output
|
||||
if response_model:
|
||||
try:
|
||||
parsed_result = response_model.model_validate(result)
|
||||
except ValidationError:
|
||||
traceback.print_exc()
|
||||
return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response"))
|
||||
else:
|
||||
parsed_result = result
|
||||
# Emit the validated result
|
||||
if parsed_result:
|
||||
if isinstance(parsed_result, BaseModel):
|
||||
return json_like(parsed_result)
|
||||
return parsed_result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def nicenessify(priority:int, pid:int|None=None):
|
||||
try:
|
||||
pid = os.getpid() if pid is None else pid
|
||||
ps = psutil.Process(pid)
|
||||
if os.name == 'posix':
|
||||
ps.nice(priority)
|
||||
except Exception as e:
|
||||
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
||||
pass
|
||||
|
||||
116
docs/TRAFFIC_VIEWER.md
Normal file
116
docs/TRAFFIC_VIEWER.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Traffic Viewer - JSON Event Format
|
||||
|
||||
The traffic viewer is now fully integrated. 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 NFProxy from './pages/NFProxy';
|
||||
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
||||
import TrafficViewer from './pages/NFProxy/TrafficViewer';
|
||||
import TrafficViewerMain from './pages/TrafficViewer';
|
||||
import { useAuthStore } from './js/store';
|
||||
|
||||
function App() {
|
||||
@@ -172,7 +174,9 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
|
||||
</Route>
|
||||
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
|
||||
<Route path=":srv" element={<ServiceDetailsNFProxy />} />
|
||||
<Route path=":srv/traffic" element={<TrafficViewer />} />
|
||||
</Route>
|
||||
<Route path="traffic" element={<TrafficViewerMain />} />
|
||||
<Route path="firewall" element={<Firewall />} />
|
||||
<Route path="porthijack" element={<PortHijack />} />
|
||||
<Route path="*" element={<HomeRedirector />} />
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useState } from 'react';
|
||||
import { RegexAddForm } from '../js/models';
|
||||
import { b64decode, b64encode, okNotify } from '../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex } from './NFRegex/utils';
|
||||
|
||||
type RegexAddInfo = {
|
||||
regex:string,
|
||||
mode:string,
|
||||
is_case_insensitive:boolean,
|
||||
deactive:boolean
|
||||
}
|
||||
|
||||
function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:string }) {
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
regex:"",
|
||||
mode:"C",
|
||||
is_case_insensitive:false,
|
||||
deactive:false
|
||||
},
|
||||
validate:{
|
||||
regex: (value) => value !== "" ? null : "Regex is required",
|
||||
mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode",
|
||||
}
|
||||
})
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = (values:RegexAddInfo) => {
|
||||
setSubmitLoading(true)
|
||||
|
||||
const request:RegexAddForm = {
|
||||
is_case_sensitive: !values.is_case_insensitive,
|
||||
service_id: service,
|
||||
mode: values.mode?values.mode:"B",
|
||||
regex: b64encode(values.regex),
|
||||
active: !values.deactive
|
||||
}
|
||||
setSubmitLoading(false)
|
||||
nfregex.regexesadd(request).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`)
|
||||
}else if (res.toLowerCase() === "invalid regex"){
|
||||
setSubmitLoading(false)
|
||||
form.setFieldError("regex", "Invalid Regex")
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Error: [ "+res+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
<TextInput
|
||||
label="Regex"
|
||||
placeholder="[A-Z0-9]{31}="
|
||||
{...form.getInputProps('regex')}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Case insensitive"
|
||||
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Deactivate"
|
||||
{...form.getInputProps('deactive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Select
|
||||
data={[
|
||||
{ value: 'C', label: 'Client -> Server' },
|
||||
{ value: 'S', label: 'Server -> Client' },
|
||||
{ value: 'B', label: 'Both (Client <-> Server)' },
|
||||
]}
|
||||
label="Choose the source of the packets to filter"
|
||||
variant="filled"
|
||||
{...form.getInputProps('mode')}
|
||||
/>
|
||||
<Group align="right" mt="md">
|
||||
<Button loading={submitLoading} type="submit">Add Filter</Button>
|
||||
</Group>
|
||||
|
||||
<Space h="md" />
|
||||
|
||||
{error?<>
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" /></>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddNewRegex;
|
||||
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useState } from 'react';
|
||||
import { RegexAddForm } from '../js/models';
|
||||
import { b64decode, b64encode, okNotify } from '../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex } from './NFRegex/utils';
|
||||
|
||||
type RegexAddInfo = {
|
||||
regex:string,
|
||||
mode:string,
|
||||
is_case_insensitive:boolean,
|
||||
deactive:boolean
|
||||
}
|
||||
|
||||
function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:string }) {
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
regex:"",
|
||||
mode:"C",
|
||||
is_case_insensitive:false,
|
||||
deactive:false
|
||||
},
|
||||
validate:{
|
||||
regex: (value) => value !== "" ? null : "Regex is required",
|
||||
mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode",
|
||||
}
|
||||
})
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = (values:RegexAddInfo) => {
|
||||
setSubmitLoading(true)
|
||||
|
||||
const request:RegexAddForm = {
|
||||
is_case_sensitive: !values.is_case_insensitive,
|
||||
service_id: service,
|
||||
mode: values.mode?values.mode:"B",
|
||||
regex: b64encode(values.regex),
|
||||
active: !values.deactive
|
||||
}
|
||||
setSubmitLoading(false)
|
||||
nfregex.regexesadd(request).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`)
|
||||
}else if (res.toLowerCase() === "invalid regex"){
|
||||
setSubmitLoading(false)
|
||||
form.setFieldError("regex", "Invalid Regex")
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Error: [ "+res+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
<TextInput
|
||||
label="Regex"
|
||||
placeholder="[A-Z0-9]{31}="
|
||||
{...form.getInputProps('regex')}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Case insensitive"
|
||||
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Deactivate"
|
||||
{...form.getInputProps('deactive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Select
|
||||
data={[
|
||||
{ value: 'C', label: 'Client -> Server' },
|
||||
{ value: 'S', label: 'Server -> Client' },
|
||||
{ value: 'B', label: 'Both (Client <-> Server)' },
|
||||
]}
|
||||
label="Choose the source of the packets to filter"
|
||||
variant="filled"
|
||||
{...form.getInputProps('mode')}
|
||||
/>
|
||||
<Group align="right" mt="md">
|
||||
<Button loading={submitLoading} type="submit">Add Filter</Button>
|
||||
</Group>
|
||||
|
||||
<Space h="md" />
|
||||
|
||||
{error?<>
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" /></>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddNewRegex;
|
||||
|
||||
@@ -1,82 +1,82 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core';
|
||||
import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils';
|
||||
import { AiFillHome } from "react-icons/ai"
|
||||
import { useNavigate } from 'react-router';
|
||||
import { FaLock } from 'react-icons/fa';
|
||||
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
|
||||
import { ImExit } from 'react-icons/im';
|
||||
import ResetPasswordModal from './ResetPasswordModal';
|
||||
import ResetModal from './ResetModal';
|
||||
import { MenuDropDownWithButton } from '../MainLayout';
|
||||
import { useNavbarStore } from '../../js/store';
|
||||
|
||||
|
||||
function HeaderPage(props: any) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const { navOpened, toggleNav } = useNavbarStore()
|
||||
|
||||
const logout_action = () => {
|
||||
logout().then(r => {
|
||||
window.location.reload()
|
||||
}).catch(r => {
|
||||
errorNotify("Logout failed!",`Error: ${r}`)
|
||||
})
|
||||
}
|
||||
|
||||
const go_to_home = () => {
|
||||
navigator(`/${getMainPath()}`)
|
||||
}
|
||||
|
||||
const [changePasswordModal, setChangePasswordModal] = useState(false);
|
||||
const [resetFiregexModal, setResetFiregexModal] = useState(false);
|
||||
return <AppShell.Header className="firegex__header__header" {...props}>
|
||||
<Burger
|
||||
hiddenFrom='md'
|
||||
ml="lg"
|
||||
opened={navOpened}
|
||||
className="firegex__header__navbtn"
|
||||
onClick={toggleNav}
|
||||
size="sm"
|
||||
/>
|
||||
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center"}} ml={5}>
|
||||
<Box className="firegex__header__divlogo">
|
||||
<Tooltip zIndex={0} label="Home" openDelay={1000} color="dark" position="right" >
|
||||
<Image src="/header-logo.png" alt="Firegex logo" w={50} onClick={()=>navigator("/")}/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
|
||||
<Title order={2} >[Fi]*regex</Title>
|
||||
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a></p>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="flex-spacer" />
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Label>Firewall Access</Menu.Label>
|
||||
<Menu.Item leftSection={<FaLock size={14} />} onClick={() => setChangePasswordModal(true)}>Change Password</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label>Actions</Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md" />
|
||||
<Tooltip label="Home" position='bottom' color="teal">
|
||||
<ActionIcon color="teal" style={{marginRight:"10px"}}
|
||||
size="xl" radius="md" variant="filled"
|
||||
onClick={go_to_home}>
|
||||
<AiFillHome size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Logout" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled">
|
||||
<ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
|
||||
</Tooltip>
|
||||
<ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} />
|
||||
<ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} />
|
||||
<Space w="xl" />
|
||||
</AppShell.Header>
|
||||
}
|
||||
|
||||
export default HeaderPage;
|
||||
import React, { useState } from 'react';
|
||||
import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core';
|
||||
import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils';
|
||||
import { AiFillHome } from "react-icons/ai"
|
||||
import { useNavigate } from 'react-router';
|
||||
import { FaLock } from 'react-icons/fa';
|
||||
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
|
||||
import { ImExit } from 'react-icons/im';
|
||||
import ResetPasswordModal from './ResetPasswordModal';
|
||||
import ResetModal from './ResetModal';
|
||||
import { MenuDropDownWithButton } from '../MainLayout';
|
||||
import { useNavbarStore } from '../../js/store';
|
||||
|
||||
|
||||
function HeaderPage(props: any) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const { navOpened, toggleNav } = useNavbarStore()
|
||||
|
||||
const logout_action = () => {
|
||||
logout().then(r => {
|
||||
window.location.reload()
|
||||
}).catch(r => {
|
||||
errorNotify("Logout failed!",`Error: ${r}`)
|
||||
})
|
||||
}
|
||||
|
||||
const go_to_home = () => {
|
||||
navigator(`/${getMainPath()}`)
|
||||
}
|
||||
|
||||
const [changePasswordModal, setChangePasswordModal] = useState(false);
|
||||
const [resetFiregexModal, setResetFiregexModal] = useState(false);
|
||||
return <AppShell.Header className="firegex__header__header" {...props}>
|
||||
<Burger
|
||||
hiddenFrom='md'
|
||||
ml="lg"
|
||||
opened={navOpened}
|
||||
className="firegex__header__navbtn"
|
||||
onClick={toggleNav}
|
||||
size="sm"
|
||||
/>
|
||||
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center"}} ml={5}>
|
||||
<Box className="firegex__header__divlogo">
|
||||
<Tooltip zIndex={0} label="Home" openDelay={1000} color="dark" position="right" >
|
||||
<Image src="/header-logo.png" alt="Firegex logo" w={50} onClick={()=>navigator("/")}/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
|
||||
<Title order={2} >[Fi]*regex</Title>
|
||||
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a></p>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="flex-spacer" />
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Label>Firewall Access</Menu.Label>
|
||||
<Menu.Item leftSection={<FaLock size={14} />} onClick={() => setChangePasswordModal(true)}>Change Password</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label>Actions</Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md" />
|
||||
<Tooltip label="Home" position='bottom' color="teal">
|
||||
<ActionIcon color="teal" style={{marginRight:"10px"}}
|
||||
size="xl" radius="md" variant="filled"
|
||||
onClick={go_to_home}>
|
||||
<AiFillHome size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Logout" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled">
|
||||
<ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
|
||||
</Tooltip>
|
||||
<ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} />
|
||||
<ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} />
|
||||
<Space w="xl" />
|
||||
</AppShell.Header>
|
||||
}
|
||||
|
||||
export default HeaderPage;
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
import { useEffect } from 'react';
|
||||
import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core';
|
||||
import { AppShell } from '@mantine/core';
|
||||
import NavBar from './NavBar';
|
||||
import HeaderPage from './Header';
|
||||
import { getMainPath } from '../js/utils';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useNavbarStore } from '../js/store';
|
||||
import { HiMenu } from "react-icons/hi";
|
||||
|
||||
|
||||
function MainLayout({ children }:{ children:any }) {
|
||||
const { navOpened } = useNavbarStore()
|
||||
const location = useLocation()
|
||||
useEffect(()=>{
|
||||
if (location.pathname !== "/"){
|
||||
sessionStorage.setItem('home_section', getMainPath())
|
||||
}
|
||||
},[location.pathname])
|
||||
return <AppShell
|
||||
header={{ height: 70 }}
|
||||
navbar={{ width: 300 , breakpoint: "md", collapsed: { mobile: !navOpened } }}
|
||||
p="md"
|
||||
>
|
||||
<HeaderPage />
|
||||
<NavBar />
|
||||
<AppShell.Main>
|
||||
<Container size="lg">
|
||||
{children}
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
<Space h="lg" />
|
||||
|
||||
</AppShell>
|
||||
|
||||
}
|
||||
|
||||
export default MainLayout;
|
||||
|
||||
export const MenuDropDownWithButton = ({children}:{children:any}) => <Menu withArrow>
|
||||
<Menu.Target>
|
||||
<Tooltip label="More options" color="gray">
|
||||
<ActionIcon variant='transparent'>
|
||||
<HiMenu size={24} color='#FFF'/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{children}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
import { useEffect } from 'react';
|
||||
import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core';
|
||||
import { AppShell } from '@mantine/core';
|
||||
import NavBar from './NavBar';
|
||||
import HeaderPage from './Header';
|
||||
import { getMainPath } from '../js/utils';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useNavbarStore } from '../js/store';
|
||||
import { HiMenu } from "react-icons/hi";
|
||||
|
||||
|
||||
function MainLayout({ children }:{ children:any }) {
|
||||
const { navOpened } = useNavbarStore()
|
||||
const location = useLocation()
|
||||
useEffect(()=>{
|
||||
if (location.pathname !== "/"){
|
||||
sessionStorage.setItem('home_section', getMainPath())
|
||||
}
|
||||
},[location.pathname])
|
||||
return <AppShell
|
||||
header={{ height: 70 }}
|
||||
navbar={{ width: 300 , breakpoint: "md", collapsed: { mobile: !navOpened } }}
|
||||
p="md"
|
||||
>
|
||||
<HeaderPage />
|
||||
<NavBar />
|
||||
<AppShell.Main>
|
||||
<Container size="lg">
|
||||
{children}
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
<Space h="lg" />
|
||||
|
||||
</AppShell>
|
||||
|
||||
}
|
||||
|
||||
export default MainLayout;
|
||||
|
||||
export const MenuDropDownWithButton = ({children}:{children:any}) => <Menu withArrow>
|
||||
<Menu.Target>
|
||||
<Tooltip label="More options" color="gray">
|
||||
<ActionIcon variant='transparent'>
|
||||
<HiMenu size={24} color='#FFF'/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{children}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@@ -1,139 +1,139 @@
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfproxy, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfproxy.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
{edit?null:<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>}
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfproxy, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfproxy.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
{edit?null:<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>}
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
|
||||
@@ -1,164 +1,164 @@
|
||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfproxy, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected } from "react-icons/tb";
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
import { FaPencilAlt } from "react-icons/fa";
|
||||
import { ExceptionWarning } from '../ExceptionWarning';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<ExceptionWarning service_id={service.service_id} />
|
||||
<Space w="sm"/>
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfproxy, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected } from "react-icons/tb";
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
import { FaPencilAlt } from "react-icons/fa";
|
||||
import { ExceptionWarning } from '../ExceptionWarning';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<ExceptionWarning service_id={service.service_id} />
|
||||
<Space w="sm"/>
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,175 +1,182 @@
|
||||
import { PyFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
service_id:string,
|
||||
name:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_filters:number,
|
||||
edited_packets:number,
|
||||
blocked_packets:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfproxy","services"]
|
||||
|
||||
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
|
||||
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters"],
|
||||
queryFn:() => nfproxy.servicepyfilters(service_id)
|
||||
})
|
||||
|
||||
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
|
||||
queryFn:() => nfproxy.getpyfilterscode(service_id)
|
||||
})
|
||||
|
||||
export const nfproxy = {
|
||||
services: async () => {
|
||||
return await getapi("nfproxy/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}`) as Service;
|
||||
},
|
||||
pyfilterenable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
pyfilterdisable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfproxy/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicepyfilters: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
getpyfilterscode: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
||||
},
|
||||
setpyfilterscode: async (service_id:string, code:string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
|
||||
|
||||
# From here we can import the DataTypes that we want to use:
|
||||
# The data type must be specified in the filter functions
|
||||
# And will also interally be used to decide when call some filters and how aggregate data
|
||||
from firegex.nfproxy.models import RawPacket
|
||||
|
||||
# global context in this execution is dedicated to a single TCP stream
|
||||
# - This code will be executed once at the TCP stream start
|
||||
# - The filter will be called for each packet in the stream
|
||||
# - You can store in global context some data you need, but exceeding with data stored could be dangerous
|
||||
# - At the end of the stream the global context will be destroyed
|
||||
|
||||
from firegex.nfproxy import pyfilter
|
||||
# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
|
||||
|
||||
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
|
||||
# - The filter must return one of the following values:
|
||||
# - ACCEPT: The packet will be accepted
|
||||
# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream)
|
||||
# - UNSTABLE_MANGLE: The packet will be mangled and accepted
|
||||
# - DROP: All the packets in this stream will be easly dropped
|
||||
|
||||
# If you want, you can use print to debug your filters, but this could slow down the filter
|
||||
|
||||
# Filter names must be unique and are specified by the name of the function wrapped by the decorator
|
||||
@pyfilter
|
||||
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
|
||||
def strange_filter(packet:RawPacket):
|
||||
# Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
|
||||
# Also is not garanteed that l4_data is the same of the packet data:
|
||||
# packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
|
||||
# Unorder packets in TCP are accepted by default, and python is not called in this case
|
||||
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
|
||||
if b"TEST_MANGLING" in packet.l4_data:
|
||||
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
|
||||
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
|
||||
return UNSTABLE_MANGLE
|
||||
# Drops the traffic
|
||||
if b"BAD DATA 1" in packet.data:
|
||||
return DROP
|
||||
# Rejects the traffic
|
||||
if b"BAD DATA 2" in packet.data:
|
||||
return REJECT
|
||||
# Accepts the traffic (default if None is returned)
|
||||
return ACCEPT
|
||||
|
||||
# Example with a higher level of abstraction
|
||||
@pyfilter
|
||||
def http_filter(http:HTTPRequest):
|
||||
if http.method == "GET" and "test" in http.url:
|
||||
return REJECT
|
||||
|
||||
# ADVANCED OPTIONS
|
||||
# You can specify some additional options on the streaming managment
|
||||
# pyproxy will automatically store all the packets (already ordered by the c++ binary):
|
||||
#
|
||||
# If the stream is too big, you can specify what actions to take:
|
||||
# This can be done defining some variables in the global context
|
||||
# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB)
|
||||
# NOTE: the stream size is calculated and managed indipendently by the data type handling system
|
||||
# Only types required by at least 1 filter will be stored.
|
||||
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
|
||||
# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
|
||||
# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
|
||||
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
|
||||
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic
|
||||
|
||||
from firege.nfproxy import FullStreamAction
|
||||
|
||||
# Example of a global context
|
||||
FGEX_STREAM_MAX_SIZE = 4096
|
||||
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT
|
||||
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic
|
||||
`
|
||||
import { PyFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
service_id:string,
|
||||
name:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_filters:number,
|
||||
edited_packets:number,
|
||||
blocked_packets:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfproxy","services"]
|
||||
|
||||
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
|
||||
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters"],
|
||||
queryFn:() => nfproxy.servicepyfilters(service_id)
|
||||
})
|
||||
|
||||
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
|
||||
queryFn:() => nfproxy.getpyfilterscode(service_id)
|
||||
})
|
||||
|
||||
export const nfproxy = {
|
||||
services: async () => {
|
||||
return await getapi("nfproxy/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}`) as Service;
|
||||
},
|
||||
pyfilterenable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
pyfilterdisable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfproxy/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicepyfilters: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
getpyfilterscode: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
||||
},
|
||||
setpyfilterscode: async (service_id:string, code:string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
gettraffic: async (service_id:string, limit:number = 500) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/traffic?limit=${limit}`) as { events: any[], count: number };
|
||||
},
|
||||
cleartraffic: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/traffic/clear`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
|
||||
|
||||
# From here we can import the DataTypes that we want to use:
|
||||
# The data type must be specified in the filter functions
|
||||
# And will also interally be used to decide when call some filters and how aggregate data
|
||||
from firegex.nfproxy.models import RawPacket
|
||||
|
||||
# global context in this execution is dedicated to a single TCP stream
|
||||
# - This code will be executed once at the TCP stream start
|
||||
# - The filter will be called for each packet in the stream
|
||||
# - You can store in global context some data you need, but exceeding with data stored could be dangerous
|
||||
# - At the end of the stream the global context will be destroyed
|
||||
|
||||
from firegex.nfproxy import pyfilter
|
||||
# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
|
||||
|
||||
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
|
||||
# - The filter must return one of the following values:
|
||||
# - ACCEPT: The packet will be accepted
|
||||
# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream)
|
||||
# - UNSTABLE_MANGLE: The packet will be mangled and accepted
|
||||
# - DROP: All the packets in this stream will be easly dropped
|
||||
|
||||
# If you want, you can use print to debug your filters, but this could slow down the filter
|
||||
|
||||
# Filter names must be unique and are specified by the name of the function wrapped by the decorator
|
||||
@pyfilter
|
||||
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
|
||||
def strange_filter(packet:RawPacket):
|
||||
# Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
|
||||
# Also is not garanteed that l4_data is the same of the packet data:
|
||||
# packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
|
||||
# Unorder packets in TCP are accepted by default, and python is not called in this case
|
||||
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
|
||||
if b"TEST_MANGLING" in packet.l4_data:
|
||||
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
|
||||
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
|
||||
return UNSTABLE_MANGLE
|
||||
# Drops the traffic
|
||||
if b"BAD DATA 1" in packet.data:
|
||||
return DROP
|
||||
# Rejects the traffic
|
||||
if b"BAD DATA 2" in packet.data:
|
||||
return REJECT
|
||||
# Accepts the traffic (default if None is returned)
|
||||
return ACCEPT
|
||||
|
||||
# Example with a higher level of abstraction
|
||||
@pyfilter
|
||||
def http_filter(http:HTTPRequest):
|
||||
if http.method == "GET" and "test" in http.url:
|
||||
return REJECT
|
||||
|
||||
# ADVANCED OPTIONS
|
||||
# You can specify some additional options on the streaming managment
|
||||
# pyproxy will automatically store all the packets (already ordered by the c++ binary):
|
||||
#
|
||||
# If the stream is too big, you can specify what actions to take:
|
||||
# This can be done defining some variables in the global context
|
||||
# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB)
|
||||
# NOTE: the stream size is calculated and managed indipendently by the data type handling system
|
||||
# Only types required by at least 1 filter will be stored.
|
||||
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
|
||||
# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
|
||||
# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
|
||||
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
|
||||
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic
|
||||
|
||||
from firege.nfproxy import FullStreamAction
|
||||
|
||||
# Example of a global context
|
||||
FGEX_STREAM_MAX_SIZE = 4096
|
||||
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT
|
||||
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic
|
||||
`
|
||||
|
||||
@@ -1,139 +1,139 @@
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfregex.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfregex.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
|
||||
@@ -1,158 +1,158 @@
|
||||
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
import { RegexFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { RegexAddForm } from "../../js/models"
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
name:string,
|
||||
service_id:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_packets:number,
|
||||
n_regex:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
proto?:string,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfregex","services"]
|
||||
|
||||
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
|
||||
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"regexes"],
|
||||
queryFn:() => nfregex.serviceregexes(service_id)
|
||||
})
|
||||
|
||||
export const nfregex = {
|
||||
services: async () => {
|
||||
return await getapi("nfregex/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}`) as Service;
|
||||
},
|
||||
regexdelete: async (regex_id:number) => {
|
||||
const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexenable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexdisable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfregex/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexesadd: async (data:RegexAddForm) => {
|
||||
const { status } = await postapi("nfregex/regexes",data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
serviceregexes: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
import { RegexFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { RegexAddForm } from "../../js/models"
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
name:string,
|
||||
service_id:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_packets:number,
|
||||
n_regex:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
proto?:string,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfregex","services"]
|
||||
|
||||
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
|
||||
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"regexes"],
|
||||
queryFn:() => nfregex.serviceregexes(service_id)
|
||||
})
|
||||
|
||||
export const nfregex = {
|
||||
services: async () => {
|
||||
return await getapi("nfregex/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}`) as Service;
|
||||
},
|
||||
regexdelete: async (regex_id:number) => {
|
||||
const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexenable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexdisable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfregex/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexesadd: async (data:RegexAddForm) => {
|
||||
const { status } = await postapi("nfregex/regexes",data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
serviceregexes: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { PiWallLight } from "react-icons/pi";
|
||||
import { useNavbarStore } from "../../js/store";
|
||||
import { getMainPath } from "../../js/utils";
|
||||
import { BsRegex } from "react-icons/bs";
|
||||
import { MdVisibility } from "react-icons/md";
|
||||
|
||||
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
||||
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
|
||||
@@ -40,6 +41,7 @@ export default function NavBar() {
|
||||
<NavBarButton navigate="firewall" closeNav={closeNav} name="Firewall Rules" color="red" icon={<PiWallLight size={19} />} />
|
||||
<NavBarButton navigate="porthijack" closeNav={closeNav} name="Hijack Port to Proxy" color="blue" icon={<GrDirections size={19} />} />
|
||||
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
||||
<NavBarButton navigate="traffic" closeNav={closeNav} name="Traffic Viewer" color="cyan" icon={<MdVisibility size={19} />} />
|
||||
{/* <Box px="xs" mt="lg">
|
||||
<Title order={5}>Experimental Features 🧪</Title>
|
||||
</Box>
|
||||
|
||||
@@ -1,113 +1,113 @@
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useState } from 'react';
|
||||
import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { porthijack } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
|
||||
type ServiceAddForm = {
|
||||
name:string,
|
||||
public_port:number,
|
||||
proxy_port:number,
|
||||
proto:string,
|
||||
ip_src:string,
|
||||
ip_dst:string,
|
||||
autostart: boolean,
|
||||
}
|
||||
|
||||
function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) {
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name:"",
|
||||
public_port:80,
|
||||
proxy_port:8080,
|
||||
proto:"tcp",
|
||||
ip_src:"",
|
||||
ip_dst:"127.0.0.1",
|
||||
autostart: false,
|
||||
},
|
||||
validate:{
|
||||
name: (value) => value !== ""? null : "Service name is required",
|
||||
public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port",
|
||||
proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port",
|
||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_src: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid source IP address",
|
||||
ip_dst: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid destination IP address",
|
||||
}
|
||||
})
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, proxy_port, public_port, autostart, proto, ip_src, ip_dst }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) porthijack.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service from port ${public_port} to ${proxy_port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" />
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>
|
||||
<Box className="flex-spacer" />
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit">Add Service</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddNewService;
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useState } from 'react';
|
||||
import { okNotify, regex_ipv6_no_cidr, regex_ipv4_no_cidr } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { porthijack } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
|
||||
type ServiceAddForm = {
|
||||
name:string,
|
||||
public_port:number,
|
||||
proxy_port:number,
|
||||
proto:string,
|
||||
ip_src:string,
|
||||
ip_dst:string,
|
||||
autostart: boolean,
|
||||
}
|
||||
|
||||
function AddNewService({ opened, onClose }:{ opened:boolean, onClose:()=>void }) {
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name:"",
|
||||
public_port:80,
|
||||
proxy_port:8080,
|
||||
proto:"tcp",
|
||||
ip_src:"",
|
||||
ip_dst:"127.0.0.1",
|
||||
autostart: false,
|
||||
},
|
||||
validate:{
|
||||
name: (value) => value !== ""? null : "Service name is required",
|
||||
public_port: (value) => (value>0 && value<65536) ? null : "Invalid public port",
|
||||
proxy_port: (value) => (value>0 && value<65536) ? null : "Invalid proxy port",
|
||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_src: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid source IP address",
|
||||
ip_dst: (value) => (value.match(regex_ipv6_no_cidr) || value.match(regex_ipv4_no_cidr)) ? null : "Invalid destination IP address",
|
||||
}
|
||||
})
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, proxy_port, public_port, autostart, proto, ip_src, ip_dst }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
porthijack.servicesadd({name, proxy_port, public_port, proto, ip_src, ip_dst }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) porthijack.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service from port ${public_port} to ${proxy_port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title="Add a new service" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_src" port_name="public_port" label="Public IP Address and port (ipv4/ipv6)" />
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_dst" port_name="proxy_port" label="Proxy/Internal IP Address and port (ipv4/ipv6)" />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>
|
||||
<Box className="flex-spacer" />
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit">Add Service</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddNewService;
|
||||
|
||||
@@ -1,152 +1,152 @@
|
||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { porthijack, Service } from '../utils';
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils';
|
||||
import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import ChangeDestination from './ChangeDestination';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { MdDoubleArrow } from "react-icons/md";
|
||||
|
||||
export default function ServiceRow({ service }:{ service:Service }) {
|
||||
|
||||
let status_color = service.active ? "teal": "red"
|
||||
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [changeDestModal, setChangeDestModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const form = useForm({
|
||||
initialValues: { proxy_port:service.proxy_port },
|
||||
validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" }
|
||||
})
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await porthijack.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.public_port} has been stopped!`)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await porthijack.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.public_port} has been started!`)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
porthijack.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="md" variant="filled">{service.active?"ENABLED":"DISABLED"}</Badge>
|
||||
<Badge color={service.proto === "tcp"?"cyan":"orange"} radius="md" size="md" variant="filled">
|
||||
{service.proto}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color="lime" radius="sm" size="lg" variant="filled">
|
||||
FROM {service.ip_src} :{service.public_port}
|
||||
</Badge>
|
||||
<Space h="sm" />
|
||||
<Badge color="blue" radius="sm" size="lg" variant="filled">
|
||||
<Box className="center-flex">
|
||||
TO {service.ip_dst} :{service.proxy_port}
|
||||
</Box>
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Label><b>Rename service</b></Menu.Label>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Menu.Label><b>Change destination</b></Menu.Label>
|
||||
<Menu.Item leftSection={<BsArrowRepeat size={18} />} onClick={()=>setChangeDestModal(true)}>Change hijacking destination</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={!service.active}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={service.active}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.public_port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<ChangeDestination
|
||||
onClose={()=>setChangeDestModal(false)}
|
||||
opened={changeDestModal}
|
||||
service={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { porthijack, Service } from '../utils';
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify } from '../../../js/utils';
|
||||
import { BsArrowRepeat, BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import ChangeDestination from './ChangeDestination';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { MdDoubleArrow } from "react-icons/md";
|
||||
|
||||
export default function ServiceRow({ service }:{ service:Service }) {
|
||||
|
||||
let status_color = service.active ? "teal": "red"
|
||||
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [changeDestModal, setChangeDestModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const form = useForm({
|
||||
initialValues: { proxy_port:service.proxy_port },
|
||||
validate:{ proxy_port: (value) => (value > 0 && value < 65536)? null : "Invalid proxy port" }
|
||||
})
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await porthijack.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.public_port} has been stopped!`)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.public_port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await porthijack.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.public_port} has been started!`)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.public_port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
porthijack.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="md" variant="filled">{service.active?"ENABLED":"DISABLED"}</Badge>
|
||||
<Badge color={service.proto === "tcp"?"cyan":"orange"} radius="md" size="md" variant="filled">
|
||||
{service.proto}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color="lime" radius="sm" size="lg" variant="filled">
|
||||
FROM {service.ip_src} :{service.public_port}
|
||||
</Badge>
|
||||
<Space h="sm" />
|
||||
<Badge color="blue" radius="sm" size="lg" variant="filled">
|
||||
<Box className="center-flex">
|
||||
TO {service.ip_dst} :{service.proxy_port}
|
||||
</Box>
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Label><b>Rename service</b></Menu.Label>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Menu.Label><b>Change destination</b></Menu.Label>
|
||||
<Menu.Item leftSection={<BsArrowRepeat size={18} />} onClick={()=>setChangeDestModal(true)}>Change hijacking destination</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={!service.active}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={service.active}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.public_port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<ChangeDestination
|
||||
onClose={()=>setChangeDestModal(false)}
|
||||
opened={changeDestModal}
|
||||
service={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
import { ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
export type GeneralStats = {
|
||||
services:number
|
||||
}
|
||||
|
||||
export type Service = {
|
||||
name:string,
|
||||
service_id:string,
|
||||
active:boolean,
|
||||
proto: string,
|
||||
ip_src: string,
|
||||
ip_dst: string,
|
||||
proxy_port: number,
|
||||
public_port: number,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
public_port:number,
|
||||
proxy_port:number,
|
||||
proto:string,
|
||||
ip_src: string,
|
||||
ip_dst: string,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = ServerResponse & { service_id: string }
|
||||
|
||||
export const queryKey = ["porthijack","services"]
|
||||
|
||||
export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services})
|
||||
|
||||
export const porthijack = {
|
||||
services: async () : Promise<Service[]> => {
|
||||
return await getapi("porthijack/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`porthijack/services/${service_id}`) as Service;
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("porthijack/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => {
|
||||
return await putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse;
|
||||
}
|
||||
import { ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
export type GeneralStats = {
|
||||
services:number
|
||||
}
|
||||
|
||||
export type Service = {
|
||||
name:string,
|
||||
service_id:string,
|
||||
active:boolean,
|
||||
proto: string,
|
||||
ip_src: string,
|
||||
ip_dst: string,
|
||||
proxy_port: number,
|
||||
public_port: number,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
public_port:number,
|
||||
proxy_port:number,
|
||||
proto:string,
|
||||
ip_src: string,
|
||||
ip_dst: string,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = ServerResponse & { service_id: string }
|
||||
|
||||
export const queryKey = ["porthijack","services"]
|
||||
|
||||
export const porthijackServiceQuery = () => useQuery({queryKey, queryFn:porthijack.services})
|
||||
|
||||
export const porthijack = {
|
||||
services: async () : Promise<Service[]> => {
|
||||
return await getapi("porthijack/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`porthijack/services/${service_id}`) as Service;
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`porthijack/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`porthijack/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`porthijack/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("porthijack/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`porthijack/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
changedestination: async (service_id:string, ip_dst:string, proxy_port:number) => {
|
||||
return await putapi(`porthijack/services/${service_id}/change-destination`, {proxy_port, ip_dst}) as ServerResponse;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,44 @@
|
||||
import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { PyFilter } from '../../js/models';
|
||||
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||
import { FaPause, FaPlay } from 'react-icons/fa';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { nfproxy } from '../NFProxy/utils';
|
||||
import { FaPencilAlt } from 'react-icons/fa';
|
||||
|
||||
export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) {
|
||||
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const changeRegexStatus = () => {
|
||||
(filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`)
|
||||
}else{
|
||||
errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
return <Box my="sm" display="flex" style={{alignItems:"center"}}>
|
||||
|
||||
<Box className="firegex__regexview__pyfilter_text" style={{ width: "100%", alignItems: "center"}} display="flex" >
|
||||
<Badge size="sm" radius="lg" mr="sm" color={filterInfo.active?"lime":"red"} variant="filled" />
|
||||
{filterInfo.name}
|
||||
<Box className='flex-spacer' />
|
||||
<Space w="xs" />
|
||||
{isMedium?<>
|
||||
<Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge>
|
||||
<Space w="lg" />
|
||||
</>:null}
|
||||
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"}>
|
||||
<ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="lg" radius="md" variant="filled">
|
||||
{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
}
|
||||
import { Text, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { PyFilter } from '../../js/models';
|
||||
import { errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||
import { FaPause, FaPlay } from 'react-icons/fa';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { nfproxy } from '../NFProxy/utils';
|
||||
import { FaPencilAlt } from 'react-icons/fa';
|
||||
|
||||
export default function PyFilterView({ filterInfo }:{ filterInfo:PyFilter }) {
|
||||
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const changeRegexStatus = () => {
|
||||
(filterInfo.active?nfproxy.pyfilterdisable:nfproxy.pyfilterenable)(filterInfo.service_id, filterInfo.name).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivated":"activated"} successfully!`,`Filter '${filterInfo.name}' has been ${filterInfo.active?"deactivated":"activated"}!`)
|
||||
}else{
|
||||
errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Filter ${filterInfo.name} ${filterInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
return <Box my="sm" display="flex" style={{alignItems:"center"}}>
|
||||
|
||||
<Box className="firegex__regexview__pyfilter_text" style={{ width: "100%", alignItems: "center"}} display="flex" >
|
||||
<Badge size="sm" radius="lg" mr="sm" color={filterInfo.active?"lime":"red"} variant="filled" />
|
||||
{filterInfo.name}
|
||||
<Box className='flex-spacer' />
|
||||
<Space w="xs" />
|
||||
{isMedium?<>
|
||||
<Badge size="md" radius="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 2}} /> {filterInfo.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="md" color="orange" variant="filled"><FaPencilAlt style={{ marginBottom: -1, marginRight: 2}} /> {filterInfo.edited_packets}</Badge>
|
||||
<Space w="lg" />
|
||||
</>:null}
|
||||
<Tooltip label={filterInfo.active?"Deactivate":"Activate"} zIndex={0} color={filterInfo.active?"orange":"teal"}>
|
||||
<ActionIcon color={filterInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="lg" radius="md" variant="filled">
|
||||
{filterInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
}
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { RegexFilter } from '../../js/models';
|
||||
import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||
import { BsTrashFill } from "react-icons/bs"
|
||||
import YesNoModal from '../YesNoModal';
|
||||
import { FaPause, FaPlay } from 'react-icons/fa';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
|
||||
import { nfregex } from '../NFRegex/utils';
|
||||
|
||||
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
|
||||
|
||||
const mode_string = regexInfo.mode === "C"? "C -> S":
|
||||
regexInfo.mode === "S"? "S -> C":
|
||||
regexInfo.mode === "B"? "C <-> S": "🤔"
|
||||
|
||||
let regex_expr = b64decode(regexInfo.regex);
|
||||
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
|
||||
const deleteRegex = () => {
|
||||
nfregex.regexdelete(regexInfo.id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`)
|
||||
}else{
|
||||
errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
const changeRegexStatus = () => {
|
||||
(regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`)
|
||||
}else{
|
||||
errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
return <Box className="firegex__regexview__box">
|
||||
<Box>
|
||||
<Box className='center-flex' style={{width: "100%"}}>
|
||||
<Box className="firegex__regexview__outer_regex_text">
|
||||
<Text className="firegex__regexview__regex_text" onClick={()=>{
|
||||
clipboard.copy(regex_expr)
|
||||
okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`)
|
||||
}}>{regex_expr}</Text>
|
||||
</Box>
|
||||
<Space w="xs" />
|
||||
<Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"}>
|
||||
<ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
|
||||
>{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Delete regex" zIndex={0} color="red" >
|
||||
<ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled">
|
||||
<BsTrashFill size={22} /></ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box display="flex" mt="sm" ml="xs">
|
||||
<Badge size="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {regexInfo.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color={regexInfo.active?"lime":"red"} variant="filled">{regexInfo.active?"ACTIVE":"DISABLED"}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color={regexInfo.is_case_sensitive?"grape":"pink"} variant="filled">{regexInfo.is_case_sensitive?"Strict":"Loose"}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color="blue" variant="filled">{mode_string}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this regex?'
|
||||
description={`You are going to delete the regex '${regex_expr}'.`}
|
||||
onClose={()=>setDeleteModal(false)}
|
||||
action={deleteRegex}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
|
||||
</Box>
|
||||
}
|
||||
|
||||
export default RegexView;
|
||||
import { Text, Title, Badge, Space, ActionIcon, Tooltip, Box } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { RegexFilter } from '../../js/models';
|
||||
import { b64decode, errorNotify, isMediumScreen, okNotify } from '../../js/utils';
|
||||
import { BsTrashFill } from "react-icons/bs"
|
||||
import YesNoModal from '../YesNoModal';
|
||||
import { FaPause, FaPlay } from 'react-icons/fa';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
|
||||
import { nfregex } from '../NFRegex/utils';
|
||||
|
||||
function RegexView({ regexInfo }:{ regexInfo:RegexFilter }) {
|
||||
|
||||
const mode_string = regexInfo.mode === "C"? "C -> S":
|
||||
regexInfo.mode === "S"? "S -> C":
|
||||
regexInfo.mode === "B"? "C <-> S": "🤔"
|
||||
|
||||
let regex_expr = b64decode(regexInfo.regex);
|
||||
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
|
||||
const deleteRegex = () => {
|
||||
nfregex.regexdelete(regexInfo.id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Regex ${regex_expr} deleted successfully!`,`Regex '${regex_expr}' ID:${regexInfo.id} has been deleted!`)
|
||||
}else{
|
||||
errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Regex ${regex_expr} deleation failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
const changeRegexStatus = () => {
|
||||
(regexInfo.active?nfregex.regexdisable:nfregex.regexenable)(regexInfo.id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivated":"activated"} successfully!`,`Regex with id '${regexInfo.id}' has been ${regexInfo.active?"deactivated":"activated"}!`)
|
||||
}else{
|
||||
errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${res}`)
|
||||
}
|
||||
}).catch( err => errorNotify(`Regex ${regex_expr} ${regexInfo.active?"deactivation":"activation"} failed!`,`Error: ${err}`))
|
||||
}
|
||||
|
||||
return <Box className="firegex__regexview__box">
|
||||
<Box>
|
||||
<Box className='center-flex' style={{width: "100%"}}>
|
||||
<Box className="firegex__regexview__outer_regex_text">
|
||||
<Text className="firegex__regexview__regex_text" onClick={()=>{
|
||||
clipboard.copy(regex_expr)
|
||||
okNotify("Regex copied to clipboard!",`The regex '${regex_expr}' has been copied to the clipboard!`)
|
||||
}}>{regex_expr}</Text>
|
||||
</Box>
|
||||
<Space w="xs" />
|
||||
<Tooltip label={regexInfo.active?"Deactivate":"Activate"} zIndex={0} color={regexInfo.active?"orange":"teal"}>
|
||||
<ActionIcon color={regexInfo.active?"orange":"teal"} onClick={changeRegexStatus} size="xl" radius="md" variant="filled"
|
||||
>{regexInfo.active?<FaPause size="20px" />:<FaPlay size="20px" />}</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Delete regex" zIndex={0} color="red" >
|
||||
<ActionIcon color="red" onClick={()=>setDeleteModal(true)} size="xl" radius="md" variant="filled">
|
||||
<BsTrashFill size={22} /></ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box display="flex" mt="sm" ml="xs">
|
||||
<Badge size="md" color="yellow" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {regexInfo.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color={regexInfo.active?"lime":"red"} variant="filled">{regexInfo.active?"ACTIVE":"DISABLED"}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color={regexInfo.is_case_sensitive?"grape":"pink"} variant="filled">{regexInfo.is_case_sensitive?"Strict":"Loose"}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" color="blue" variant="filled">{mode_string}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this regex?'
|
||||
description={`You are going to delete the regex '${regex_expr}'.`}
|
||||
onClose={()=>setDeleteModal(false)}
|
||||
action={deleteRegex}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
|
||||
</Box>
|
||||
}
|
||||
|
||||
export default RegexView;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Button, Group, Modal } from '@mantine/core';
|
||||
import React from 'react';
|
||||
|
||||
function YesNoModal( { title, description, action, onClose, opened}:{ title:string, description:string, onClose:()=>void, action:()=>void, opened:boolean} ){
|
||||
|
||||
return <Modal size="xl" title={title} opened={opened} onClose={onClose} centered>
|
||||
{description}
|
||||
<Group justify='flex-end' mt="md">
|
||||
<Button onClick={()=>{
|
||||
onClose()
|
||||
action()
|
||||
}} color="teal" type="submit">Yes</Button>
|
||||
<Button onClick={onClose} color="red" type="submit">No</Button>
|
||||
|
||||
</Group>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
import { Button, Group, Modal } from '@mantine/core';
|
||||
import React from 'react';
|
||||
|
||||
function YesNoModal( { title, description, action, onClose, opened}:{ title:string, description:string, onClose:()=>void, action:()=>void, opened:boolean} ){
|
||||
|
||||
return <Modal size="xl" title={title} opened={opened} onClose={onClose} centered>
|
||||
{description}
|
||||
<Group justify='flex-end' mt="md">
|
||||
<Button onClick={()=>{
|
||||
onClose()
|
||||
action()
|
||||
}} color="teal" type="submit">Yes</Button>
|
||||
<Button onClick={onClose} color="red" type="submit">No</Button>
|
||||
|
||||
</Group>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
export default YesNoModal;
|
||||
@@ -1,227 +1,227 @@
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
import { ImCross } from "react-icons/im";
|
||||
import { TiTick } from "react-icons/ti"
|
||||
import { Navigate } from "react-router";
|
||||
import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models";
|
||||
import { Buffer } from "buffer"
|
||||
import { QueryClient, useQuery } from "@tanstack/react-query";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { io } from "socket.io-client";
|
||||
import { useAuthStore, useSessionStore } from "./store";
|
||||
|
||||
export const IS_DEV = import.meta.env.DEV
|
||||
|
||||
export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$";
|
||||
export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$";
|
||||
export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$"
|
||||
export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
|
||||
export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$"
|
||||
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
|
||||
export const DEV_IP_BACKEND = "127.0.0.1:4444"
|
||||
|
||||
export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes
|
||||
|
||||
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
|
||||
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
|
||||
|
||||
export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
|
||||
return value as E;
|
||||
}
|
||||
|
||||
export const socketio = import.meta.env.DEV?
|
||||
io("ws://"+DEV_IP_BACKEND, {
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
}):
|
||||
io({
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
})
|
||||
|
||||
export const queryClient = new QueryClient({ defaultOptions: { queries: {
|
||||
staleTime: Infinity
|
||||
} }})
|
||||
|
||||
export function getErrorMessage(e: any) {
|
||||
let error = "Unknown error";
|
||||
if(typeof e == "string") return e
|
||||
if (e.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
error = e.response.data.error;
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
error = e.message || e.error;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") {
|
||||
if (e.status){
|
||||
return e.status
|
||||
}
|
||||
if (e.detail){
|
||||
if (typeof e.detail == "string")
|
||||
return e.detail
|
||||
if (e.detail[0] && e.detail[0].msg)
|
||||
return e.detail[0].msg
|
||||
}
|
||||
if (e.error){
|
||||
return e.error
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
|
||||
export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
|
||||
return await new Promise((resolve, reject) => {
|
||||
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
|
||||
method: method,
|
||||
credentials: "same-origin",
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}),
|
||||
"Authorization" : "Bearer " + useAuthStore.getState().getAccessToken()
|
||||
},
|
||||
body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined
|
||||
}).then(res => {
|
||||
if(res.status === 401) window.location.reload()
|
||||
if(res.status === 406) resolve({status:"Wrong Password"})
|
||||
if(!res.ok){
|
||||
const errorDefault = res.statusText
|
||||
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
|
||||
}
|
||||
res.text().then(t => {
|
||||
try{
|
||||
resolve(JSON.parse(t))
|
||||
}catch(e){
|
||||
resolve(t)
|
||||
}
|
||||
}).catch( err => reject(err))
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function getapi(path:string):Promise<any>{
|
||||
return await genericapi("GET",path)
|
||||
}
|
||||
|
||||
export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise<any>{
|
||||
return await genericapi("POST",path,data,is_form)
|
||||
}
|
||||
|
||||
export async function deleteapi(path:string):Promise<any>{
|
||||
return await genericapi("DELETE",path)
|
||||
}
|
||||
|
||||
export async function putapi(path:string,data:any):Promise<any>{
|
||||
return await genericapi("PUT",path,data)
|
||||
}
|
||||
|
||||
export function getMainPath(){
|
||||
const paths = window.location.pathname.split("/")
|
||||
if (paths.length > 1) return paths[1]
|
||||
return ""
|
||||
}
|
||||
|
||||
export function HomeRedirector(){
|
||||
const section = useSessionStore.getState().getHomeSection();
|
||||
const path = section?`/${section}`:`/nfregex`
|
||||
return <Navigate to={path} replace/>
|
||||
}
|
||||
|
||||
export async function resetfiregex(delete_data:boolean = false){
|
||||
const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse;
|
||||
return (status === "ok"?undefined:status)
|
||||
}
|
||||
|
||||
export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces)
|
||||
|
||||
export async function getipinterfaces(){
|
||||
return await getapi("interfaces") as IpInterface[];
|
||||
}
|
||||
|
||||
export async function getstatus(){
|
||||
return await getapi(`status`) as ServerStatusResponse;
|
||||
}
|
||||
|
||||
export async function logout(){
|
||||
useAuthStore.getState().clearAccessToken();
|
||||
}
|
||||
|
||||
export async function setpassword(data:PasswordSend) {
|
||||
const { status, access_token } = await postapi("set-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function changepassword(data:ChangePassword) {
|
||||
const { status, access_token } = await postapi("change-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function login(data:PasswordSend) {
|
||||
const from = {username: "login", password: data.password};
|
||||
const { status, access_token } = await postapi("login",from,true) as LoginResponse;
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status;
|
||||
}
|
||||
|
||||
export function errorNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'red',
|
||||
icon: <ImCross />,
|
||||
});
|
||||
}
|
||||
|
||||
export function okNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'teal',
|
||||
icon: <TiTick />,
|
||||
});
|
||||
}
|
||||
|
||||
export const makeid = (length:number) => {
|
||||
let result = '';
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const charactersLength = characters.length;
|
||||
let counter = 0;
|
||||
while (counter < length) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
counter += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function b64encode(data:number[]|string){
|
||||
return Buffer.from(data).toString('base64')
|
||||
}
|
||||
|
||||
export function b64decode(regexB64:string){
|
||||
return Buffer.from(regexB64, "base64").toString()
|
||||
}
|
||||
|
||||
export function isMediumScreen(){
|
||||
return useMediaQuery('(min-width: 600px)');
|
||||
}
|
||||
|
||||
export function isLargeScreen(){
|
||||
return useMediaQuery('(min-width: 992px)');
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
import { ImCross } from "react-icons/im";
|
||||
import { TiTick } from "react-icons/ti"
|
||||
import { Navigate } from "react-router";
|
||||
import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models";
|
||||
import { Buffer } from "buffer"
|
||||
import { QueryClient, useQuery } from "@tanstack/react-query";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { io } from "socket.io-client";
|
||||
import { useAuthStore, useSessionStore } from "./store";
|
||||
|
||||
export const IS_DEV = import.meta.env.DEV
|
||||
|
||||
export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$";
|
||||
export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$";
|
||||
export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$"
|
||||
export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
|
||||
export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$"
|
||||
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
|
||||
export const DEV_IP_BACKEND = "127.0.0.1:4444"
|
||||
|
||||
export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes
|
||||
|
||||
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
|
||||
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
|
||||
|
||||
export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
|
||||
return value as E;
|
||||
}
|
||||
|
||||
export const socketio = import.meta.env.DEV?
|
||||
io("ws://"+DEV_IP_BACKEND, {
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
}):
|
||||
io({
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
})
|
||||
|
||||
export const queryClient = new QueryClient({ defaultOptions: { queries: {
|
||||
staleTime: Infinity
|
||||
} }})
|
||||
|
||||
export function getErrorMessage(e: any) {
|
||||
let error = "Unknown error";
|
||||
if(typeof e == "string") return e
|
||||
if (e.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
error = e.response.data.error;
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
error = e.message || e.error;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") {
|
||||
if (e.status){
|
||||
return e.status
|
||||
}
|
||||
if (e.detail){
|
||||
if (typeof e.detail == "string")
|
||||
return e.detail
|
||||
if (e.detail[0] && e.detail[0].msg)
|
||||
return e.detail[0].msg
|
||||
}
|
||||
if (e.error){
|
||||
return e.error
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
|
||||
export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
|
||||
return await new Promise((resolve, reject) => {
|
||||
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
|
||||
method: method,
|
||||
credentials: "same-origin",
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}),
|
||||
"Authorization" : "Bearer " + useAuthStore.getState().getAccessToken()
|
||||
},
|
||||
body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined
|
||||
}).then(res => {
|
||||
if(res.status === 401) window.location.reload()
|
||||
if(res.status === 406) resolve({status:"Wrong Password"})
|
||||
if(!res.ok){
|
||||
const errorDefault = res.statusText
|
||||
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
|
||||
}
|
||||
res.text().then(t => {
|
||||
try{
|
||||
resolve(JSON.parse(t))
|
||||
}catch(e){
|
||||
resolve(t)
|
||||
}
|
||||
}).catch( err => reject(err))
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function getapi(path:string):Promise<any>{
|
||||
return await genericapi("GET",path)
|
||||
}
|
||||
|
||||
export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise<any>{
|
||||
return await genericapi("POST",path,data,is_form)
|
||||
}
|
||||
|
||||
export async function deleteapi(path:string):Promise<any>{
|
||||
return await genericapi("DELETE",path)
|
||||
}
|
||||
|
||||
export async function putapi(path:string,data:any):Promise<any>{
|
||||
return await genericapi("PUT",path,data)
|
||||
}
|
||||
|
||||
export function getMainPath(){
|
||||
const paths = window.location.pathname.split("/")
|
||||
if (paths.length > 1) return paths[1]
|
||||
return ""
|
||||
}
|
||||
|
||||
export function HomeRedirector(){
|
||||
const section = useSessionStore.getState().getHomeSection();
|
||||
const path = section?`/${section}`:`/nfregex`
|
||||
return <Navigate to={path} replace/>
|
||||
}
|
||||
|
||||
export async function resetfiregex(delete_data:boolean = false){
|
||||
const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse;
|
||||
return (status === "ok"?undefined:status)
|
||||
}
|
||||
|
||||
export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces)
|
||||
|
||||
export async function getipinterfaces(){
|
||||
return await getapi("interfaces") as IpInterface[];
|
||||
}
|
||||
|
||||
export async function getstatus(){
|
||||
return await getapi(`status`) as ServerStatusResponse;
|
||||
}
|
||||
|
||||
export async function logout(){
|
||||
useAuthStore.getState().clearAccessToken();
|
||||
}
|
||||
|
||||
export async function setpassword(data:PasswordSend) {
|
||||
const { status, access_token } = await postapi("set-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function changepassword(data:ChangePassword) {
|
||||
const { status, access_token } = await postapi("change-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function login(data:PasswordSend) {
|
||||
const from = {username: "login", password: data.password};
|
||||
const { status, access_token } = await postapi("login",from,true) as LoginResponse;
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status;
|
||||
}
|
||||
|
||||
export function errorNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'red',
|
||||
icon: <ImCross />,
|
||||
});
|
||||
}
|
||||
|
||||
export function okNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'teal',
|
||||
icon: <TiTick />,
|
||||
});
|
||||
}
|
||||
|
||||
export const makeid = (length:number) => {
|
||||
let result = '';
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const charactersLength = characters.length;
|
||||
let counter = 0;
|
||||
while (counter < length) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
counter += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function b64encode(data:number[]|string){
|
||||
return Buffer.from(data).toString('base64')
|
||||
}
|
||||
|
||||
export function b64decode(regexB64:string){
|
||||
return Buffer.from(regexB64, "base64").toString()
|
||||
}
|
||||
|
||||
export function isMediumScreen(){
|
||||
return useMediaQuery('(min-width: 600px)');
|
||||
}
|
||||
|
||||
export function isLargeScreen(){
|
||||
return useMediaQuery('(min-width: 992px)');
|
||||
}
|
||||
@@ -1,237 +1,243 @@
|
||||
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import PyFilterView from '../../components/PyFilterView';
|
||||
import { TbPlugConnected } from 'react-icons/tb';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { FaPython } from "react-icons/fa";
|
||||
import { FiFileText } from "react-icons/fi";
|
||||
import { ModalLog } from '../../components/ModalLog';
|
||||
import { useListState } from '@mantine/hooks';
|
||||
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
export default function ServiceDetailsNFProxy() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const services = nfproxyServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
const [openLogModal, setOpenLogModal] = useState(false)
|
||||
const [logData, logDataSetters] = useListState<string>([]);
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
if (srv){
|
||||
if (openLogModal){
|
||||
logDataSetters.setState([])
|
||||
socketio.emit("nfproxy-outstream-join", { service: srv });
|
||||
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
|
||||
logDataSetters.append(data)
|
||||
});
|
||||
}else{
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
return () => {
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
}
|
||||
}, [openLogModal, srv])
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={filtersList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<ExceptionWarning service_id={srv} />
|
||||
<Space w="sm" />
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Show logs" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
|
||||
<FiFileText size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider my="xl" />
|
||||
|
||||
{filterCode.data?<>
|
||||
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
|
||||
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
|
||||
</>: null}
|
||||
|
||||
{(!filtersList.data || filtersList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
|
||||
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
|
||||
}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
<ModalLog
|
||||
opened={openLogModal}
|
||||
close={()=>setOpenLogModal(false)}
|
||||
title={`Logs for service ${serviceInfo.name}`}
|
||||
data={logData.join("")}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import PyFilterView from '../../components/PyFilterView';
|
||||
import { TbPlugConnected } from 'react-icons/tb';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { FaPython } from "react-icons/fa";
|
||||
import { FiFileText } from "react-icons/fi";
|
||||
import { ModalLog } from '../../components/ModalLog';
|
||||
import { useListState } from '@mantine/hooks';
|
||||
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
export default function ServiceDetailsNFProxy() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const services = nfproxyServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
const [openLogModal, setOpenLogModal] = useState(false)
|
||||
const [logData, logDataSetters] = useListState<string>([]);
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
if (srv){
|
||||
if (openLogModal){
|
||||
logDataSetters.setState([])
|
||||
socketio.emit("nfproxy-outstream-join", { service: srv });
|
||||
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
|
||||
logDataSetters.append(data)
|
||||
});
|
||||
}else{
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
return () => {
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
}
|
||||
}, [openLogModal, srv])
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={filtersList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<ExceptionWarning service_id={srv} />
|
||||
<Space w="sm" />
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Show logs" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
|
||||
<FiFileText size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs"/>
|
||||
<Tooltip label="Traffic viewer" zIndex={0} color="grape">
|
||||
<ActionIcon color="grape" size="lg" radius="md" onClick={()=>navigate(`/nfproxy/${srv}/traffic`)} variant="filled">
|
||||
<MdDoubleArrow size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider my="xl" />
|
||||
|
||||
{filterCode.data?<>
|
||||
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
|
||||
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
|
||||
</>: null}
|
||||
|
||||
{(!filtersList.data || filtersList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
|
||||
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
|
||||
}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
<ModalLog
|
||||
opened={openLogModal}
|
||||
close={()=>setOpenLogModal(false)}
|
||||
title={`Logs for service ${serviceInfo.name}`}
|
||||
data={logData.join("")}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
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 { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFProxy/ServiceRow';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected, TbReload } from 'react-icons/tb';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
|
||||
import { MdUploadFile } from "react-icons/md";
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useFileDialog } from '@mantine/hooks';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
export default function NFProxy({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfproxyServiceQuery()
|
||||
const fileDialog = useFileDialog({
|
||||
accept: ".py",
|
||||
multiple: false,
|
||||
resetOnOpen: true,
|
||||
onChange: (files) => {
|
||||
if (files?.length??0 > 0)
|
||||
setFile(files![0])
|
||||
}
|
||||
});
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
useEffect(() => {
|
||||
if (!srv) return
|
||||
const service = services.data?.find(s => s.service_id === srv)
|
||||
if (!service) return
|
||||
if (file){
|
||||
console.log("Uploading code")
|
||||
const notify_id = notifications.show(
|
||||
{
|
||||
title: "Uploading code",
|
||||
message: `Uploading code for service ${service.name}`,
|
||||
color: "blue",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: false,
|
||||
loading: true,
|
||||
}
|
||||
)
|
||||
file.text()
|
||||
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
|
||||
.then( res => {
|
||||
if (!res){
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code uploaded",
|
||||
message: `Successfully uploaded code for service ${service.name}`,
|
||||
color: "green",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}else{
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${res}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
}).catch( err => {
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${err}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}).finally(()=>{setFile(null)})
|
||||
}
|
||||
}, [file])
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
|
||||
<MdUploadFile size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
|
||||
<BsPlusLg size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
|
||||
<TbReload size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfproxy" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfproxy/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter proxy is a simulated proxy written using python with a c++ core</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Then you can create a new service and write custom filters for the service</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfproxy" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{!srv?
|
||||
<AddEditService opened={open} onClose={closeModal} />:null
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
import { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFProxy/ServiceRow';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected, TbReload } from 'react-icons/tb';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
|
||||
import { MdUploadFile } from "react-icons/md";
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useFileDialog } from '@mantine/hooks';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
export default function NFProxy({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfproxyServiceQuery()
|
||||
const fileDialog = useFileDialog({
|
||||
accept: ".py",
|
||||
multiple: false,
|
||||
resetOnOpen: true,
|
||||
onChange: (files) => {
|
||||
if (files?.length??0 > 0)
|
||||
setFile(files![0])
|
||||
}
|
||||
});
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
useEffect(() => {
|
||||
if (!srv) return
|
||||
const service = services.data?.find(s => s.service_id === srv)
|
||||
if (!service) return
|
||||
if (file){
|
||||
console.log("Uploading code")
|
||||
const notify_id = notifications.show(
|
||||
{
|
||||
title: "Uploading code",
|
||||
message: `Uploading code for service ${service.name}`,
|
||||
color: "blue",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: false,
|
||||
loading: true,
|
||||
}
|
||||
)
|
||||
file.text()
|
||||
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
|
||||
.then( res => {
|
||||
if (!res){
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code uploaded",
|
||||
message: `Successfully uploaded code for service ${service.name}`,
|
||||
color: "green",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}else{
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${res}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
}).catch( err => {
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${err}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}).finally(()=>{setFile(null)})
|
||||
}
|
||||
}, [file])
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
|
||||
<MdUploadFile size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
|
||||
<BsPlusLg size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
|
||||
<TbReload size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfproxy" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfproxy/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter proxy is a simulated proxy written using python with a c++ core</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Then you can create a new service and write custom filters for the service</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfproxy" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{!srv?
|
||||
<AddEditService opened={open} onClose={closeModal} />:null
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,194 +1,194 @@
|
||||
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import RegexView from '../../components/RegexView';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { VscRegex } from 'react-icons/vsc';
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
|
||||
export default function ServiceDetailsNFRegex() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
const services = nfregexServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const regexesList = nfregexServiceRegexesQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={regexesList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider my="xl" />
|
||||
{(!regexesList.data || regexesList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
|
||||
<Space h="xl" /> <Space h="xl" />
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Add a new regex" zIndex={0} color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</>:
|
||||
<Grid>
|
||||
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import RegexView from '../../components/RegexView';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { VscRegex } from 'react-icons/vsc';
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
|
||||
export default function ServiceDetailsNFRegex() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
const services = nfregexServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const regexesList = nfregexServiceRegexesQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={regexesList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider my="xl" />
|
||||
{(!regexesList.data || regexesList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
|
||||
<Space h="xl" /> <Space h="xl" />
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Add a new regex" zIndex={0} color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</>:
|
||||
<Grid>
|
||||
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,100 +1,100 @@
|
||||
import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg, BsRegex } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFRegex/ServiceRow';
|
||||
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaFilter } from 'react-icons/fa';
|
||||
import { FaServer } from "react-icons/fa6";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
function NFRegex({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfregexServiceQuery()
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFRegex Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Add a new regex" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfregex" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfregex/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter Regex allows you to filter traffic using regexes</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start a service, add your regexes and it's already done!</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfregex" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{srv?
|
||||
<AddNewRegex opened={open} onClose={closeModal} service={srv} />:
|
||||
<AddEditService opened={open} onClose={closeModal} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
export default NFRegex;
|
||||
import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg, BsRegex } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFRegex/ServiceRow';
|
||||
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaFilter } from 'react-icons/fa';
|
||||
import { FaServer } from "react-icons/fa6";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
function NFRegex({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfregexServiceQuery()
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFRegex Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Add a new regex" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfregex" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfregex/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter Regex allows you to filter traffic using regexes</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start a service, add your regexes and it's already done!</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfregex" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{srv?
|
||||
<AddNewRegex opened={open} onClose={closeModal} service={srv} />:
|
||||
<AddEditService opened={open} onClose={closeModal} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
export default NFRegex;
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import ServiceRow from '../../components/PortHijack/ServiceRow';
|
||||
import { porthijackServiceQuery } from '../../components/PortHijack/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddNewService from '../../components/PortHijack/AddNewService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaServer } from 'react-icons/fa';
|
||||
import { GrDirections } from 'react-icons/gr';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
function PortHijack() {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const services = porthijackServiceQuery()
|
||||
|
||||
useEffect(()=>{
|
||||
if(services.isError)
|
||||
errorNotify("Porthijack Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="porthijack" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="porthijack" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
<AddNewService opened={open} onClose={closeModal} />
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
|
||||
export default PortHijack;
|
||||
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import ServiceRow from '../../components/PortHijack/ServiceRow';
|
||||
import { porthijackServiceQuery } from '../../components/PortHijack/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddNewService from '../../components/PortHijack/AddNewService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaServer } from 'react-icons/fa';
|
||||
import { GrDirections } from 'react-icons/gr';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
function PortHijack() {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const services = porthijackServiceQuery()
|
||||
|
||||
useEffect(()=>{
|
||||
if(services.isError)
|
||||
errorNotify("Porthijack Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="porthijack" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="porthijack" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
<AddNewService opened={open} onClose={closeModal} />
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
|
||||
export default PortHijack;
|
||||
|
||||
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": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
|
||||
2
run.py
2
run.py
@@ -268,7 +268,7 @@ def write_compose(skip_password = True):
|
||||
"firewall": {
|
||||
"restart": "unless-stopped",
|
||||
"container_name": "firegex",
|
||||
"build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}",
|
||||
"build" if g.build else "image": "." if g.build else f"ghcr.io/ilyastar9999/firegex:{args.version}",
|
||||
"network_mode": "host",
|
||||
"environment": [
|
||||
f"PORT={args.port}",
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
0,4090.616
|
||||
1,2211.62
|
||||
2,1165.45
|
||||
3,849.39
|
||||
4,828.635
|
||||
5,741.537
|
||||
6,632.721
|
||||
7,624.772
|
||||
8,529.234
|
||||
9,469.688
|
||||
10,336.33
|
||||
11,427.783
|
||||
12,400.662
|
||||
13,335.086
|
||||
14,342.042
|
||||
15,307.283
|
||||
16,239.694
|
||||
17,295.163
|
||||
18,285.787
|
||||
19,254.402
|
||||
20,250.553
|
||||
21,227.146
|
||||
22,238.747
|
||||
23,234.718
|
||||
24,210.484
|
||||
25,210.697
|
||||
26,205.943
|
||||
27,202.568
|
||||
28,194.341
|
||||
29,189.916
|
||||
30,154.228
|
||||
31,168.922
|
||||
32,173.623
|
||||
33,125.431
|
||||
34,162.154
|
||||
35,149.865
|
||||
36,150.088
|
||||
37,146.085
|
||||
38,137.182
|
||||
39,138.686
|
||||
40,136.302
|
||||
41,132.707
|
||||
42,100.928
|
||||
43,126.414
|
||||
44,125.271
|
||||
45,117.839
|
||||
46,89.494
|
||||
47,116.939
|
||||
48,112.517
|
||||
49,111.369
|
||||
50,108.568
|
||||
0,4090.616
|
||||
1,2211.62
|
||||
2,1165.45
|
||||
3,849.39
|
||||
4,828.635
|
||||
5,741.537
|
||||
6,632.721
|
||||
7,624.772
|
||||
8,529.234
|
||||
9,469.688
|
||||
10,336.33
|
||||
11,427.783
|
||||
12,400.662
|
||||
13,335.086
|
||||
14,342.042
|
||||
15,307.283
|
||||
16,239.694
|
||||
17,295.163
|
||||
18,285.787
|
||||
19,254.402
|
||||
20,250.553
|
||||
21,227.146
|
||||
22,238.747
|
||||
23,234.718
|
||||
24,210.484
|
||||
25,210.697
|
||||
26,205.943
|
||||
27,202.568
|
||||
28,194.341
|
||||
29,189.916
|
||||
30,154.228
|
||||
31,168.922
|
||||
32,173.623
|
||||
33,125.431
|
||||
34,162.154
|
||||
35,149.865
|
||||
36,150.088
|
||||
37,146.085
|
||||
38,137.182
|
||||
39,138.686
|
||||
40,136.302
|
||||
41,132.707
|
||||
42,100.928
|
||||
43,126.414
|
||||
44,125.271
|
||||
45,117.839
|
||||
46,89.494
|
||||
47,116.939
|
||||
48,112.517
|
||||
49,111.369
|
||||
50,108.568
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,3789.988
|
||||
1,2069.487
|
||||
2,1484.554
|
||||
3,956.972
|
||||
4,1052.873
|
||||
5,739.658
|
||||
6,534.722
|
||||
7,638.524
|
||||
8,573.833
|
||||
9,531.658
|
||||
10,476.167
|
||||
11,443.746
|
||||
12,406.027
|
||||
13,385.739
|
||||
14,341.563
|
||||
15,318.699
|
||||
16,303.722
|
||||
17,284.924
|
||||
18,284.336
|
||||
19,267.32
|
||||
20,202.74
|
||||
21,243.849
|
||||
22,226.082
|
||||
23,214.348
|
||||
24,216.8
|
||||
25,188.98
|
||||
26,158.68
|
||||
27,166.556
|
||||
28,148.287
|
||||
29,149.681
|
||||
30,177.043
|
||||
31,175.321
|
||||
32,165.312
|
||||
33,166.943
|
||||
34,159.026
|
||||
35,156.759
|
||||
36,150.216
|
||||
37,144.932
|
||||
38,146.088
|
||||
39,135.897
|
||||
40,136.99
|
||||
41,128.557
|
||||
42,100.307
|
||||
43,103.249
|
||||
44,123.49
|
||||
45,120.39
|
||||
46,118.055
|
||||
47,115.0
|
||||
48,112.593
|
||||
49,109.55
|
||||
50,109.512
|
||||
0,3789.988
|
||||
1,2069.487
|
||||
2,1484.554
|
||||
3,956.972
|
||||
4,1052.873
|
||||
5,739.658
|
||||
6,534.722
|
||||
7,638.524
|
||||
8,573.833
|
||||
9,531.658
|
||||
10,476.167
|
||||
11,443.746
|
||||
12,406.027
|
||||
13,385.739
|
||||
14,341.563
|
||||
15,318.699
|
||||
16,303.722
|
||||
17,284.924
|
||||
18,284.336
|
||||
19,267.32
|
||||
20,202.74
|
||||
21,243.849
|
||||
22,226.082
|
||||
23,214.348
|
||||
24,216.8
|
||||
25,188.98
|
||||
26,158.68
|
||||
27,166.556
|
||||
28,148.287
|
||||
29,149.681
|
||||
30,177.043
|
||||
31,175.321
|
||||
32,165.312
|
||||
33,166.943
|
||||
34,159.026
|
||||
35,156.759
|
||||
36,150.216
|
||||
37,144.932
|
||||
38,146.088
|
||||
39,135.897
|
||||
40,136.99
|
||||
41,128.557
|
||||
42,100.307
|
||||
43,103.249
|
||||
44,123.49
|
||||
45,120.39
|
||||
46,118.055
|
||||
47,115.0
|
||||
48,112.593
|
||||
49,109.55
|
||||
50,109.512
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,4216.05
|
||||
1,4239.598
|
||||
2,2418.527
|
||||
3,2227.8
|
||||
4,2045.351
|
||||
5,2066.161
|
||||
6,2214.416
|
||||
7,2052.845
|
||||
8,2195.199
|
||||
9,2186.867
|
||||
10,2147.534
|
||||
11,2186.652
|
||||
12,2178.036
|
||||
13,2182.151
|
||||
14,2185.324
|
||||
15,1812.911
|
||||
16,2144.689
|
||||
17,2163.525
|
||||
18,2073.89
|
||||
19,2071.682
|
||||
20,2153.502
|
||||
21,2144.04
|
||||
22,2118.517
|
||||
23,2141.19
|
||||
24,2167.103
|
||||
25,2168.631
|
||||
26,2165.555
|
||||
27,2158.424
|
||||
28,2188.376
|
||||
29,2165.311
|
||||
30,2168.158
|
||||
31,2108.045
|
||||
32,2121.414
|
||||
33,2022.533
|
||||
34,1888.759
|
||||
35,2022.837
|
||||
36,2015.042
|
||||
37,1920.401
|
||||
38,2005.037
|
||||
39,2028.856
|
||||
40,2010.43
|
||||
41,1522.342
|
||||
42,1525.635
|
||||
43,1912.05
|
||||
44,1920.256
|
||||
45,1753.645
|
||||
46,1476.977
|
||||
47,1888.645
|
||||
48,1949.103
|
||||
49,1684.633
|
||||
50,1493.935
|
||||
0,4216.05
|
||||
1,4239.598
|
||||
2,2418.527
|
||||
3,2227.8
|
||||
4,2045.351
|
||||
5,2066.161
|
||||
6,2214.416
|
||||
7,2052.845
|
||||
8,2195.199
|
||||
9,2186.867
|
||||
10,2147.534
|
||||
11,2186.652
|
||||
12,2178.036
|
||||
13,2182.151
|
||||
14,2185.324
|
||||
15,1812.911
|
||||
16,2144.689
|
||||
17,2163.525
|
||||
18,2073.89
|
||||
19,2071.682
|
||||
20,2153.502
|
||||
21,2144.04
|
||||
22,2118.517
|
||||
23,2141.19
|
||||
24,2167.103
|
||||
25,2168.631
|
||||
26,2165.555
|
||||
27,2158.424
|
||||
28,2188.376
|
||||
29,2165.311
|
||||
30,2168.158
|
||||
31,2108.045
|
||||
32,2121.414
|
||||
33,2022.533
|
||||
34,1888.759
|
||||
35,2022.837
|
||||
36,2015.042
|
||||
37,1920.401
|
||||
38,2005.037
|
||||
39,2028.856
|
||||
40,2010.43
|
||||
41,1522.342
|
||||
42,1525.635
|
||||
43,1912.05
|
||||
44,1920.256
|
||||
45,1753.645
|
||||
46,1476.977
|
||||
47,1888.645
|
||||
48,1949.103
|
||||
49,1684.633
|
||||
50,1493.935
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,4203.31
|
||||
1,4283.392
|
||||
2,2383.415
|
||||
3,2419.701
|
||||
4,2038.823
|
||||
5,2038.0
|
||||
6,2160.869
|
||||
7,2192.641
|
||||
8,2216.766
|
||||
9,2762.56
|
||||
10,2160.398
|
||||
11,2147.886
|
||||
12,2146.47
|
||||
13,2158.101
|
||||
14,2154.025
|
||||
15,1997.694
|
||||
16,2028.288
|
||||
17,2005.373
|
||||
18,2153.945
|
||||
19,2190.799
|
||||
20,2169.302
|
||||
21,2139.842
|
||||
22,2155.307
|
||||
23,2152.223
|
||||
24,2124.155
|
||||
25,2103.135
|
||||
26,2148.053
|
||||
27,2163.366
|
||||
28,2122.339
|
||||
29,2064.701
|
||||
30,2134.748
|
||||
31,1632.533
|
||||
32,2082.309
|
||||
33,1878.795
|
||||
34,2009.28
|
||||
35,1987.424
|
||||
36,1748.364
|
||||
37,1725.66
|
||||
38,1967.877
|
||||
39,1854.637
|
||||
40,1903.963
|
||||
41,1987.138
|
||||
42,1532.547
|
||||
43,1569.27
|
||||
44,1535.941
|
||||
45,1941.715
|
||||
46,2014.504
|
||||
47,2005.794
|
||||
48,2022.972
|
||||
49,1740.836
|
||||
50,1726.444
|
||||
0,4203.31
|
||||
1,4283.392
|
||||
2,2383.415
|
||||
3,2419.701
|
||||
4,2038.823
|
||||
5,2038.0
|
||||
6,2160.869
|
||||
7,2192.641
|
||||
8,2216.766
|
||||
9,2762.56
|
||||
10,2160.398
|
||||
11,2147.886
|
||||
12,2146.47
|
||||
13,2158.101
|
||||
14,2154.025
|
||||
15,1997.694
|
||||
16,2028.288
|
||||
17,2005.373
|
||||
18,2153.945
|
||||
19,2190.799
|
||||
20,2169.302
|
||||
21,2139.842
|
||||
22,2155.307
|
||||
23,2152.223
|
||||
24,2124.155
|
||||
25,2103.135
|
||||
26,2148.053
|
||||
27,2163.366
|
||||
28,2122.339
|
||||
29,2064.701
|
||||
30,2134.748
|
||||
31,1632.533
|
||||
32,2082.309
|
||||
33,1878.795
|
||||
34,2009.28
|
||||
35,1987.424
|
||||
36,1748.364
|
||||
37,1725.66
|
||||
38,1967.877
|
||||
39,1854.637
|
||||
40,1903.963
|
||||
41,1987.138
|
||||
42,1532.547
|
||||
43,1569.27
|
||||
44,1535.941
|
||||
45,1941.715
|
||||
46,2014.504
|
||||
47,2005.794
|
||||
48,2022.972
|
||||
49,1740.836
|
||||
50,1726.444
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,710.619
|
||||
1,887.877
|
||||
2,981.431
|
||||
3,1081.412
|
||||
4,1038.514
|
||||
5,1029.805
|
||||
6,928.317
|
||||
7,1130.938
|
||||
8,1165.42
|
||||
9,925.632
|
||||
10,949.483
|
||||
11,1021.973
|
||||
12,903.878
|
||||
13,1001.53
|
||||
14,895.351
|
||||
15,1026.722
|
||||
16,634.727
|
||||
17,744.758
|
||||
18,978.59
|
||||
19,962.375
|
||||
20,997.471
|
||||
21,929.785
|
||||
22,1200.83
|
||||
23,1257.741
|
||||
24,772.729
|
||||
25,683.913
|
||||
26,1188.17
|
||||
27,919.961
|
||||
28,922.225
|
||||
29,1066.286
|
||||
30,979.399
|
||||
31,978.917
|
||||
32,988.415
|
||||
33,1061.523
|
||||
34,942.85
|
||||
35,1045.949
|
||||
36,883.941
|
||||
37,958.41
|
||||
38,989.523
|
||||
39,1001.121
|
||||
40,1080.079
|
||||
41,1151.938
|
||||
42,1221.644
|
||||
43,991.855
|
||||
44,1088.344
|
||||
45,973.641
|
||||
46,952.35
|
||||
47,1089.644
|
||||
48,939.615
|
||||
49,1258.419
|
||||
50,949.414
|
||||
0,710.619
|
||||
1,887.877
|
||||
2,981.431
|
||||
3,1081.412
|
||||
4,1038.514
|
||||
5,1029.805
|
||||
6,928.317
|
||||
7,1130.938
|
||||
8,1165.42
|
||||
9,925.632
|
||||
10,949.483
|
||||
11,1021.973
|
||||
12,903.878
|
||||
13,1001.53
|
||||
14,895.351
|
||||
15,1026.722
|
||||
16,634.727
|
||||
17,744.758
|
||||
18,978.59
|
||||
19,962.375
|
||||
20,997.471
|
||||
21,929.785
|
||||
22,1200.83
|
||||
23,1257.741
|
||||
24,772.729
|
||||
25,683.913
|
||||
26,1188.17
|
||||
27,919.961
|
||||
28,922.225
|
||||
29,1066.286
|
||||
30,979.399
|
||||
31,978.917
|
||||
32,988.415
|
||||
33,1061.523
|
||||
34,942.85
|
||||
35,1045.949
|
||||
36,883.941
|
||||
37,958.41
|
||||
38,989.523
|
||||
39,1001.121
|
||||
40,1080.079
|
||||
41,1151.938
|
||||
42,1221.644
|
||||
43,991.855
|
||||
44,1088.344
|
||||
45,973.641
|
||||
46,952.35
|
||||
47,1089.644
|
||||
48,939.615
|
||||
49,1258.419
|
||||
50,949.414
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,3245.763
|
||||
1,3283.646
|
||||
2,3741.157
|
||||
3,3691.206
|
||||
4,3365.134
|
||||
5,3691.457
|
||||
6,3354.807
|
||||
7,3526.728
|
||||
8,3252.62
|
||||
9,3551.086
|
||||
10,3561.506
|
||||
11,3525.577
|
||||
12,2776.064
|
||||
13,3541.86
|
||||
14,3501.34
|
||||
15,3692.092
|
||||
16,3637.166
|
||||
17,3617.031
|
||||
18,3700.092
|
||||
19,3176.831
|
||||
20,3368.038
|
||||
21,3716.577
|
||||
22,3452.917
|
||||
23,3617.604
|
||||
24,3651.796
|
||||
25,3552.053
|
||||
26,3843.18
|
||||
27,3720.406
|
||||
28,3431.1
|
||||
29,3578.973
|
||||
30,3561.994
|
||||
31,3524.566
|
||||
32,3567.537
|
||||
33,3626.767
|
||||
34,3498.361
|
||||
35,3621.396
|
||||
36,3297.839
|
||||
37,3541.207
|
||||
38,3560.364
|
||||
39,3589.746
|
||||
40,3686.673
|
||||
41,3463.811
|
||||
42,3428.408
|
||||
43,3753.139
|
||||
44,3368.89
|
||||
45,3324.876
|
||||
46,3614.895
|
||||
47,3245.942
|
||||
48,3257.925
|
||||
49,3200.585
|
||||
50,3321.55
|
||||
0,3245.763
|
||||
1,3283.646
|
||||
2,3741.157
|
||||
3,3691.206
|
||||
4,3365.134
|
||||
5,3691.457
|
||||
6,3354.807
|
||||
7,3526.728
|
||||
8,3252.62
|
||||
9,3551.086
|
||||
10,3561.506
|
||||
11,3525.577
|
||||
12,2776.064
|
||||
13,3541.86
|
||||
14,3501.34
|
||||
15,3692.092
|
||||
16,3637.166
|
||||
17,3617.031
|
||||
18,3700.092
|
||||
19,3176.831
|
||||
20,3368.038
|
||||
21,3716.577
|
||||
22,3452.917
|
||||
23,3617.604
|
||||
24,3651.796
|
||||
25,3552.053
|
||||
26,3843.18
|
||||
27,3720.406
|
||||
28,3431.1
|
||||
29,3578.973
|
||||
30,3561.994
|
||||
31,3524.566
|
||||
32,3567.537
|
||||
33,3626.767
|
||||
34,3498.361
|
||||
35,3621.396
|
||||
36,3297.839
|
||||
37,3541.207
|
||||
38,3560.364
|
||||
39,3589.746
|
||||
40,3686.673
|
||||
41,3463.811
|
||||
42,3428.408
|
||||
43,3753.139
|
||||
44,3368.89
|
||||
45,3324.876
|
||||
46,3614.895
|
||||
47,3245.942
|
||||
48,3257.925
|
||||
49,3200.585
|
||||
50,3321.55
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,1790.382
|
||||
1,1933.881
|
||||
2,1941.564
|
||||
3,1926.518
|
||||
4,1945.295
|
||||
5,1734.462
|
||||
6,2009.994
|
||||
7,2007.538
|
||||
8,2004.825
|
||||
9,1848.551
|
||||
10,1836.558
|
||||
11,1977.19
|
||||
12,1987.207
|
||||
13,2007.422
|
||||
14,1994.914
|
||||
15,1982.997
|
||||
16,1955.828
|
||||
17,1705.883
|
||||
18,1983.501
|
||||
19,1951.311
|
||||
20,1921.772
|
||||
21,1956.908
|
||||
22,1948.865
|
||||
23,1929.387
|
||||
24,1814.539
|
||||
25,2084.284
|
||||
26,1830.901
|
||||
27,1946.713
|
||||
28,1958.238
|
||||
29,1906.573
|
||||
30,1895.341
|
||||
31,1986.09
|
||||
32,1943.785
|
||||
33,1879.917
|
||||
34,1946.029
|
||||
35,1858.958
|
||||
36,2009.44
|
||||
37,1876.749
|
||||
38,1967.254
|
||||
39,1968.595
|
||||
40,1846.438
|
||||
41,1955.897
|
||||
42,1986.446
|
||||
43,1965.143
|
||||
44,1963.016
|
||||
45,1890.88
|
||||
46,1998.801
|
||||
47,1682.048
|
||||
48,2023.688
|
||||
49,1982.952
|
||||
50,1993.641
|
||||
0,1790.382
|
||||
1,1933.881
|
||||
2,1941.564
|
||||
3,1926.518
|
||||
4,1945.295
|
||||
5,1734.462
|
||||
6,2009.994
|
||||
7,2007.538
|
||||
8,2004.825
|
||||
9,1848.551
|
||||
10,1836.558
|
||||
11,1977.19
|
||||
12,1987.207
|
||||
13,2007.422
|
||||
14,1994.914
|
||||
15,1982.997
|
||||
16,1955.828
|
||||
17,1705.883
|
||||
18,1983.501
|
||||
19,1951.311
|
||||
20,1921.772
|
||||
21,1956.908
|
||||
22,1948.865
|
||||
23,1929.387
|
||||
24,1814.539
|
||||
25,2084.284
|
||||
26,1830.901
|
||||
27,1946.713
|
||||
28,1958.238
|
||||
29,1906.573
|
||||
30,1895.341
|
||||
31,1986.09
|
||||
32,1943.785
|
||||
33,1879.917
|
||||
34,1946.029
|
||||
35,1858.958
|
||||
36,2009.44
|
||||
37,1876.749
|
||||
38,1967.254
|
||||
39,1968.595
|
||||
40,1846.438
|
||||
41,1955.897
|
||||
42,1986.446
|
||||
43,1965.143
|
||||
44,1963.016
|
||||
45,1890.88
|
||||
46,1998.801
|
||||
47,1682.048
|
||||
48,2023.688
|
||||
49,1982.952
|
||||
50,1993.641
|
||||
|
||||
|
@@ -1,51 +1,51 @@
|
||||
0,4007.679
|
||||
1,3963.986
|
||||
2,4222.243
|
||||
3,3640.707
|
||||
4,4388.553
|
||||
5,3636.047
|
||||
6,3644.611
|
||||
7,3547.39
|
||||
8,3412.162
|
||||
9,3632.367
|
||||
10,3536.655
|
||||
11,3820.019
|
||||
12,3677.177
|
||||
13,3366.323
|
||||
14,3353.031
|
||||
15,3392.423
|
||||
16,3330.368
|
||||
17,3363.272
|
||||
18,4027.34
|
||||
19,3467.982
|
||||
20,3607.754
|
||||
21,3767.614
|
||||
22,3340.544
|
||||
23,4086.612
|
||||
24,3784.164
|
||||
25,3496.518
|
||||
26,3543.808
|
||||
27,3453.934
|
||||
28,3546.188
|
||||
29,3458.804
|
||||
30,3728.609
|
||||
31,3697.624
|
||||
32,3698.191
|
||||
33,3673.973
|
||||
34,3690.046
|
||||
35,3663.799
|
||||
36,3540.004
|
||||
37,3857.604
|
||||
38,3426.215
|
||||
39,3704.176
|
||||
40,3796.133
|
||||
41,3604.623
|
||||
42,3650.508
|
||||
43,3501.861
|
||||
44,3685.992
|
||||
45,3623.404
|
||||
46,3728.601
|
||||
47,3844.994
|
||||
48,3820.046
|
||||
49,3680.976
|
||||
50,3797.432
|
||||
0,4007.679
|
||||
1,3963.986
|
||||
2,4222.243
|
||||
3,3640.707
|
||||
4,4388.553
|
||||
5,3636.047
|
||||
6,3644.611
|
||||
7,3547.39
|
||||
8,3412.162
|
||||
9,3632.367
|
||||
10,3536.655
|
||||
11,3820.019
|
||||
12,3677.177
|
||||
13,3366.323
|
||||
14,3353.031
|
||||
15,3392.423
|
||||
16,3330.368
|
||||
17,3363.272
|
||||
18,4027.34
|
||||
19,3467.982
|
||||
20,3607.754
|
||||
21,3767.614
|
||||
22,3340.544
|
||||
23,4086.612
|
||||
24,3784.164
|
||||
25,3496.518
|
||||
26,3543.808
|
||||
27,3453.934
|
||||
28,3546.188
|
||||
29,3458.804
|
||||
30,3728.609
|
||||
31,3697.624
|
||||
32,3698.191
|
||||
33,3673.973
|
||||
34,3690.046
|
||||
35,3663.799
|
||||
36,3540.004
|
||||
37,3857.604
|
||||
38,3426.215
|
||||
39,3704.176
|
||||
40,3796.133
|
||||
41,3604.623
|
||||
42,3650.508
|
||||
43,3501.861
|
||||
44,3685.992
|
||||
45,3623.404
|
||||
46,3728.601
|
||||
47,3844.994
|
||||
48,3820.046
|
||||
49,3680.976
|
||||
50,3797.432
|
||||
|
||||
|
@@ -1,101 +1,101 @@
|
||||
No filters,test data
|
||||
1600.27,1772.897
|
||||
1486.257,1455.93
|
||||
1534.667,1403.539
|
||||
1244.374,1665.846
|
||||
1569.867,1627.449
|
||||
1522.719,1084.153
|
||||
1391.244,1259.783
|
||||
1528.465,1282.901
|
||||
1310.989,1275.515
|
||||
1675.138,1074.39
|
||||
1393.644,1359.139
|
||||
1639.889,1162.937
|
||||
1658.168,1239.767
|
||||
1477.156,1308.195
|
||||
1224.386,1298.007
|
||||
1420.7,1087.031
|
||||
1353.746,1090.502
|
||||
1759.778,1179.381
|
||||
1414.33,1222.86
|
||||
1475.981,1295.207
|
||||
1375.197,1327.8
|
||||
1265.015,1189.121
|
||||
1335.179,1594.98
|
||||
1191.896,1271.873
|
||||
1596.418,1100.372
|
||||
1433.755,1147.945
|
||||
1213.187,1312.989
|
||||
1157.99,1153.825
|
||||
1322.314,1184.481
|
||||
1262.974,1271.012
|
||||
1266.223,1350.519
|
||||
1192.275,1199.142
|
||||
1296.164,1189.432
|
||||
1245.501,1185.107
|
||||
1293.076,1374.689
|
||||
1260.554,1384.055
|
||||
1219.219,1420.395
|
||||
1132.234,1099.141
|
||||
1129.541,1101.805
|
||||
1273.171,1210.564
|
||||
1269.415,1184.094
|
||||
1370.586,1321.974
|
||||
1303.694,1317.074
|
||||
1413.705,1380.092
|
||||
1324.827,1142.097
|
||||
1124.399,1548.557
|
||||
1137.381,1029.353
|
||||
1419.146,1326.829
|
||||
1342.397,1270.316
|
||||
1546.898,1258.933
|
||||
1268.918,1062.23
|
||||
1239.877,1234.887
|
||||
1474.269,1181.184
|
||||
1289.763,1139.728
|
||||
1387.416,1125.734
|
||||
1128.784,1278.381
|
||||
1519.4,1243.597
|
||||
1343.003,1153.18
|
||||
1547.543,1117.816
|
||||
1582.958,1594.145
|
||||
1618.213,1358.087
|
||||
1449.399,1295.487
|
||||
1373.062,1174.153
|
||||
1211.207,1346.833
|
||||
1066.275,1417.633
|
||||
1203.659,1131.727
|
||||
1129.005,1351.061
|
||||
1200.245,1615.952
|
||||
1232.596,1250.436
|
||||
1262.319,1563.46
|
||||
1127.022,1651.89
|
||||
1736.368,1561.661
|
||||
1310.858,1459.713
|
||||
1351.455,1608.494
|
||||
1156.124,1440.379
|
||||
1220.053,1267.708
|
||||
1171.428,1300.284
|
||||
1149.242,1087.453
|
||||
1213.915,1081.207
|
||||
1092.869,1402.761
|
||||
1243.623,1321.907
|
||||
1216.257,1217.721
|
||||
1221.354,1263.695
|
||||
1242.771,1241.684
|
||||
1427.276,1322.01
|
||||
1328.502,1346.21
|
||||
1275.719,1269.909
|
||||
1372.075,1451.069
|
||||
1486.541,1532.56
|
||||
1577.036,1539.804
|
||||
1628.025,1372.806
|
||||
1415.623,1239.201
|
||||
1198.632,1095.849
|
||||
1170.341,1255.875
|
||||
1214.99,1424.292
|
||||
1356.431,1135.588
|
||||
1817.822,1212.386
|
||||
1745.199,1170.863
|
||||
1779.083,1145.458
|
||||
1544.934,1076.386
|
||||
No filters,test data
|
||||
1600.27,1772.897
|
||||
1486.257,1455.93
|
||||
1534.667,1403.539
|
||||
1244.374,1665.846
|
||||
1569.867,1627.449
|
||||
1522.719,1084.153
|
||||
1391.244,1259.783
|
||||
1528.465,1282.901
|
||||
1310.989,1275.515
|
||||
1675.138,1074.39
|
||||
1393.644,1359.139
|
||||
1639.889,1162.937
|
||||
1658.168,1239.767
|
||||
1477.156,1308.195
|
||||
1224.386,1298.007
|
||||
1420.7,1087.031
|
||||
1353.746,1090.502
|
||||
1759.778,1179.381
|
||||
1414.33,1222.86
|
||||
1475.981,1295.207
|
||||
1375.197,1327.8
|
||||
1265.015,1189.121
|
||||
1335.179,1594.98
|
||||
1191.896,1271.873
|
||||
1596.418,1100.372
|
||||
1433.755,1147.945
|
||||
1213.187,1312.989
|
||||
1157.99,1153.825
|
||||
1322.314,1184.481
|
||||
1262.974,1271.012
|
||||
1266.223,1350.519
|
||||
1192.275,1199.142
|
||||
1296.164,1189.432
|
||||
1245.501,1185.107
|
||||
1293.076,1374.689
|
||||
1260.554,1384.055
|
||||
1219.219,1420.395
|
||||
1132.234,1099.141
|
||||
1129.541,1101.805
|
||||
1273.171,1210.564
|
||||
1269.415,1184.094
|
||||
1370.586,1321.974
|
||||
1303.694,1317.074
|
||||
1413.705,1380.092
|
||||
1324.827,1142.097
|
||||
1124.399,1548.557
|
||||
1137.381,1029.353
|
||||
1419.146,1326.829
|
||||
1342.397,1270.316
|
||||
1546.898,1258.933
|
||||
1268.918,1062.23
|
||||
1239.877,1234.887
|
||||
1474.269,1181.184
|
||||
1289.763,1139.728
|
||||
1387.416,1125.734
|
||||
1128.784,1278.381
|
||||
1519.4,1243.597
|
||||
1343.003,1153.18
|
||||
1547.543,1117.816
|
||||
1582.958,1594.145
|
||||
1618.213,1358.087
|
||||
1449.399,1295.487
|
||||
1373.062,1174.153
|
||||
1211.207,1346.833
|
||||
1066.275,1417.633
|
||||
1203.659,1131.727
|
||||
1129.005,1351.061
|
||||
1200.245,1615.952
|
||||
1232.596,1250.436
|
||||
1262.319,1563.46
|
||||
1127.022,1651.89
|
||||
1736.368,1561.661
|
||||
1310.858,1459.713
|
||||
1351.455,1608.494
|
||||
1156.124,1440.379
|
||||
1220.053,1267.708
|
||||
1171.428,1300.284
|
||||
1149.242,1087.453
|
||||
1213.915,1081.207
|
||||
1092.869,1402.761
|
||||
1243.623,1321.907
|
||||
1216.257,1217.721
|
||||
1221.354,1263.695
|
||||
1242.771,1241.684
|
||||
1427.276,1322.01
|
||||
1328.502,1346.21
|
||||
1275.719,1269.909
|
||||
1372.075,1451.069
|
||||
1486.541,1532.56
|
||||
1577.036,1539.804
|
||||
1628.025,1372.806
|
||||
1415.623,1239.201
|
||||
1198.632,1095.849
|
||||
1170.341,1255.875
|
||||
1214.99,1424.292
|
||||
1356.431,1135.588
|
||||
1817.822,1212.386
|
||||
1745.199,1170.863
|
||||
1779.083,1145.458
|
||||
1544.934,1076.386
|
||||
|
||||
|
@@ -1,101 +1,101 @@
|
||||
No filters,test data
|
||||
2098.666,2118.781
|
||||
2175.2,2086.957
|
||||
2177.653,1795.287
|
||||
1775.63,1745.066
|
||||
1827.78,2038.921
|
||||
1813.369,2179.81
|
||||
1988.859,2176.883
|
||||
1634.541,1704.071
|
||||
1878.829,1869.999
|
||||
1738.987,2024.959
|
||||
1920.502,1477.726
|
||||
1895.909,1732.832
|
||||
1812.012,1850.978
|
||||
1908.106,1902.953
|
||||
2112.837,1726.547
|
||||
1765.808,1710.915
|
||||
1918.121,1900.619
|
||||
1892.779,2054.93
|
||||
1852.952,2113.928
|
||||
1713.67,1770.379
|
||||
1873.637,2011.518
|
||||
1787.007,2104.061
|
||||
1764.704,2134.151
|
||||
2064.776,2073.226
|
||||
1838.23,1762.436
|
||||
1808.339,1792.41
|
||||
1756.516,1706.501
|
||||
1665.888,1610.771
|
||||
1682.272,1650.033
|
||||
1690.473,1563.995
|
||||
1997.801,1955.53
|
||||
1660.487,1669.25
|
||||
2023.106,1727.046
|
||||
1724.59,1686.137
|
||||
1697.656,1627.136
|
||||
1689.65,1571.13
|
||||
1628.35,1699.239
|
||||
1843.768,1825.739
|
||||
1715.158,1573.695
|
||||
1732.695,1875.656
|
||||
1902.818,1968.505
|
||||
1699.277,1919.737
|
||||
1618.75,2015.258
|
||||
1696.055,2014.261
|
||||
1792.486,1606.754
|
||||
1889.583,1625.965
|
||||
1716.951,1572.049
|
||||
1727.305,1649.502
|
||||
1747.618,2099.787
|
||||
1698.546,2153.363
|
||||
1723.117,1637.074
|
||||
1654.061,1721.968
|
||||
1735.332,1587.906
|
||||
1841.808,1565.797
|
||||
2006.973,1665.615
|
||||
1730.909,1883.505
|
||||
1681.954,1553.826
|
||||
1653.215,1849.824
|
||||
2072.138,1990.474
|
||||
1792.302,2176.718
|
||||
1679.381,2128.083
|
||||
1653.368,2078.013
|
||||
1399.58,2065.031
|
||||
1669.979,1815.553
|
||||
1677.346,1870.055
|
||||
1652.22,2010.441
|
||||
1870.35,1687.893
|
||||
1772.229,1857.193
|
||||
1743.552,1813.027
|
||||
1685.312,1466.505
|
||||
1863.269,1813.398
|
||||
1694.335,1889.661
|
||||
1739.016,1740.381
|
||||
1764.462,1752.725
|
||||
1702.134,2069.289
|
||||
1955.771,2176.617
|
||||
2046.117,2137.499
|
||||
1766.64,2177.955
|
||||
1733.26,2148.497
|
||||
1834.827,2161.573
|
||||
2087.089,2119.311
|
||||
2154.753,1679.596
|
||||
2073.729,1912.012
|
||||
2082.37,1841.045
|
||||
2160.86,1813.257
|
||||
1678.515,1894.864
|
||||
1758.394,1884.985
|
||||
1673.919,1732.373
|
||||
1666.474,1737.66
|
||||
1679.444,1463.082
|
||||
1684.006,2002.343
|
||||
1737.287,2026.394
|
||||
1811.305,2084.689
|
||||
2127.121,2117.391
|
||||
2139.884,1984.606
|
||||
1677.256,1770.76
|
||||
1698.544,1833.011
|
||||
1905.446,1734.777
|
||||
1913.257,1688.401
|
||||
2063.73,1667.27
|
||||
No filters,test data
|
||||
2098.666,2118.781
|
||||
2175.2,2086.957
|
||||
2177.653,1795.287
|
||||
1775.63,1745.066
|
||||
1827.78,2038.921
|
||||
1813.369,2179.81
|
||||
1988.859,2176.883
|
||||
1634.541,1704.071
|
||||
1878.829,1869.999
|
||||
1738.987,2024.959
|
||||
1920.502,1477.726
|
||||
1895.909,1732.832
|
||||
1812.012,1850.978
|
||||
1908.106,1902.953
|
||||
2112.837,1726.547
|
||||
1765.808,1710.915
|
||||
1918.121,1900.619
|
||||
1892.779,2054.93
|
||||
1852.952,2113.928
|
||||
1713.67,1770.379
|
||||
1873.637,2011.518
|
||||
1787.007,2104.061
|
||||
1764.704,2134.151
|
||||
2064.776,2073.226
|
||||
1838.23,1762.436
|
||||
1808.339,1792.41
|
||||
1756.516,1706.501
|
||||
1665.888,1610.771
|
||||
1682.272,1650.033
|
||||
1690.473,1563.995
|
||||
1997.801,1955.53
|
||||
1660.487,1669.25
|
||||
2023.106,1727.046
|
||||
1724.59,1686.137
|
||||
1697.656,1627.136
|
||||
1689.65,1571.13
|
||||
1628.35,1699.239
|
||||
1843.768,1825.739
|
||||
1715.158,1573.695
|
||||
1732.695,1875.656
|
||||
1902.818,1968.505
|
||||
1699.277,1919.737
|
||||
1618.75,2015.258
|
||||
1696.055,2014.261
|
||||
1792.486,1606.754
|
||||
1889.583,1625.965
|
||||
1716.951,1572.049
|
||||
1727.305,1649.502
|
||||
1747.618,2099.787
|
||||
1698.546,2153.363
|
||||
1723.117,1637.074
|
||||
1654.061,1721.968
|
||||
1735.332,1587.906
|
||||
1841.808,1565.797
|
||||
2006.973,1665.615
|
||||
1730.909,1883.505
|
||||
1681.954,1553.826
|
||||
1653.215,1849.824
|
||||
2072.138,1990.474
|
||||
1792.302,2176.718
|
||||
1679.381,2128.083
|
||||
1653.368,2078.013
|
||||
1399.58,2065.031
|
||||
1669.979,1815.553
|
||||
1677.346,1870.055
|
||||
1652.22,2010.441
|
||||
1870.35,1687.893
|
||||
1772.229,1857.193
|
||||
1743.552,1813.027
|
||||
1685.312,1466.505
|
||||
1863.269,1813.398
|
||||
1694.335,1889.661
|
||||
1739.016,1740.381
|
||||
1764.462,1752.725
|
||||
1702.134,2069.289
|
||||
1955.771,2176.617
|
||||
2046.117,2137.499
|
||||
1766.64,2177.955
|
||||
1733.26,2148.497
|
||||
1834.827,2161.573
|
||||
2087.089,2119.311
|
||||
2154.753,1679.596
|
||||
2073.729,1912.012
|
||||
2082.37,1841.045
|
||||
2160.86,1813.257
|
||||
1678.515,1894.864
|
||||
1758.394,1884.985
|
||||
1673.919,1732.373
|
||||
1666.474,1737.66
|
||||
1679.444,1463.082
|
||||
1684.006,2002.343
|
||||
1737.287,2026.394
|
||||
1811.305,2084.689
|
||||
2127.121,2117.391
|
||||
2139.884,1984.606
|
||||
1677.256,1770.76
|
||||
1698.544,1833.011
|
||||
1905.446,1734.777
|
||||
1913.257,1688.401
|
||||
2063.73,1667.27
|
||||
|
||||
|
@@ -1,101 +1,101 @@
|
||||
No filters,test data
|
||||
3841.832,3177.356
|
||||
3369.899,3819.926
|
||||
3884.689,2843.759
|
||||
3391.267,3106.399
|
||||
3740.054,2899.246
|
||||
3754.086,3254.525
|
||||
3284.178,3180.96
|
||||
3293.044,3356.928
|
||||
3653.05,2925.883
|
||||
3830.609,2784.715
|
||||
3691.078,3283.715
|
||||
3551.286,3437.899
|
||||
3651.296,2759.088
|
||||
3726.295,3289.184
|
||||
3860.353,3067.069
|
||||
3910.997,3764.354
|
||||
3775.794,3182.171
|
||||
3824.719,3376.774
|
||||
3245.109,2954.582
|
||||
3705.489,4101.548
|
||||
3484.114,3155.55
|
||||
3742.727,3153.767
|
||||
3964.472,3624.241
|
||||
3747.219,2787.965
|
||||
3746.575,3518.095
|
||||
3903.7,2942.676
|
||||
3888.772,3222.041
|
||||
3854.913,2479.502
|
||||
3716.801,2876.082
|
||||
3919.146,2748.543
|
||||
3908.195,2742.45
|
||||
3894.436,3135.703
|
||||
3615.381,3411.222
|
||||
3807.51,3525.049
|
||||
3197.936,3515.207
|
||||
3817.654,3505.676
|
||||
3604.482,3749.862
|
||||
4054.217,3389.18
|
||||
4064.973,3110.13
|
||||
3828.174,3994.395
|
||||
3464.949,3706.928
|
||||
3458.833,3818.998
|
||||
3447.594,3354.733
|
||||
3148.49,2938.606
|
||||
3403.617,3000.615
|
||||
3619.143,3712.188
|
||||
3676.835,3294.72
|
||||
4020.2,3668.025
|
||||
3365.03,3288.992
|
||||
3395.001,3047.487
|
||||
3444.301,3644.15
|
||||
3258.341,3412.968
|
||||
3640.787,3028.915
|
||||
3523.975,2984.702
|
||||
3661.891,3124.492
|
||||
3802.303,3098.351
|
||||
3774.646,3486.505
|
||||
3622.705,1967.98
|
||||
3508.677,2629.166
|
||||
3566.014,2717.307
|
||||
3849.619,1697.053
|
||||
3315.839,1708.413
|
||||
3423.282,2104.829
|
||||
3750.536,2822.277
|
||||
3554.167,2610.241
|
||||
3826.747,3645.146
|
||||
3892.643,2795.429
|
||||
3832.114,2572.367
|
||||
3497.325,3586.324
|
||||
3348.139,3108.224
|
||||
3317.933,2944.826
|
||||
3605.83,2890.459
|
||||
3539.072,3132.536
|
||||
3121.903,3343.355
|
||||
2942.032,3478.153
|
||||
3445.076,3762.927
|
||||
3100.771,3377.621
|
||||
3189.105,3326.58
|
||||
3281.825,3443.852
|
||||
2678.243,3830.363
|
||||
2955.651,2863.628
|
||||
2696.034,3640.54
|
||||
3370.494,3203.94
|
||||
3300.628,3755.641
|
||||
3488.021,3931.192
|
||||
3330.963,2780.609
|
||||
3154.885,2986.501
|
||||
3375.716,3359.562
|
||||
2841.549,3077.406
|
||||
3404.81,3385.657
|
||||
3757.787,3352.594
|
||||
3717.258,3264.236
|
||||
3353.01,3659.337
|
||||
3190.808,3732.121
|
||||
3165.985,3380.969
|
||||
3797.661,3264.325
|
||||
3347.68,3711.328
|
||||
3604.306,3454.656
|
||||
3615.091,3547.976
|
||||
3291.287,3115.255
|
||||
No filters,test data
|
||||
3841.832,3177.356
|
||||
3369.899,3819.926
|
||||
3884.689,2843.759
|
||||
3391.267,3106.399
|
||||
3740.054,2899.246
|
||||
3754.086,3254.525
|
||||
3284.178,3180.96
|
||||
3293.044,3356.928
|
||||
3653.05,2925.883
|
||||
3830.609,2784.715
|
||||
3691.078,3283.715
|
||||
3551.286,3437.899
|
||||
3651.296,2759.088
|
||||
3726.295,3289.184
|
||||
3860.353,3067.069
|
||||
3910.997,3764.354
|
||||
3775.794,3182.171
|
||||
3824.719,3376.774
|
||||
3245.109,2954.582
|
||||
3705.489,4101.548
|
||||
3484.114,3155.55
|
||||
3742.727,3153.767
|
||||
3964.472,3624.241
|
||||
3747.219,2787.965
|
||||
3746.575,3518.095
|
||||
3903.7,2942.676
|
||||
3888.772,3222.041
|
||||
3854.913,2479.502
|
||||
3716.801,2876.082
|
||||
3919.146,2748.543
|
||||
3908.195,2742.45
|
||||
3894.436,3135.703
|
||||
3615.381,3411.222
|
||||
3807.51,3525.049
|
||||
3197.936,3515.207
|
||||
3817.654,3505.676
|
||||
3604.482,3749.862
|
||||
4054.217,3389.18
|
||||
4064.973,3110.13
|
||||
3828.174,3994.395
|
||||
3464.949,3706.928
|
||||
3458.833,3818.998
|
||||
3447.594,3354.733
|
||||
3148.49,2938.606
|
||||
3403.617,3000.615
|
||||
3619.143,3712.188
|
||||
3676.835,3294.72
|
||||
4020.2,3668.025
|
||||
3365.03,3288.992
|
||||
3395.001,3047.487
|
||||
3444.301,3644.15
|
||||
3258.341,3412.968
|
||||
3640.787,3028.915
|
||||
3523.975,2984.702
|
||||
3661.891,3124.492
|
||||
3802.303,3098.351
|
||||
3774.646,3486.505
|
||||
3622.705,1967.98
|
||||
3508.677,2629.166
|
||||
3566.014,2717.307
|
||||
3849.619,1697.053
|
||||
3315.839,1708.413
|
||||
3423.282,2104.829
|
||||
3750.536,2822.277
|
||||
3554.167,2610.241
|
||||
3826.747,3645.146
|
||||
3892.643,2795.429
|
||||
3832.114,2572.367
|
||||
3497.325,3586.324
|
||||
3348.139,3108.224
|
||||
3317.933,2944.826
|
||||
3605.83,2890.459
|
||||
3539.072,3132.536
|
||||
3121.903,3343.355
|
||||
2942.032,3478.153
|
||||
3445.076,3762.927
|
||||
3100.771,3377.621
|
||||
3189.105,3326.58
|
||||
3281.825,3443.852
|
||||
2678.243,3830.363
|
||||
2955.651,2863.628
|
||||
2696.034,3640.54
|
||||
3370.494,3203.94
|
||||
3300.628,3755.641
|
||||
3488.021,3931.192
|
||||
3330.963,2780.609
|
||||
3154.885,2986.501
|
||||
3375.716,3359.562
|
||||
2841.549,3077.406
|
||||
3404.81,3385.657
|
||||
3757.787,3352.594
|
||||
3717.258,3264.236
|
||||
3353.01,3659.337
|
||||
3190.808,3732.121
|
||||
3165.985,3380.969
|
||||
3797.661,3264.325
|
||||
3347.68,3711.328
|
||||
3604.306,3454.656
|
||||
3615.091,3547.976
|
||||
3291.287,3115.255
|
||||
|
||||
|
@@ -1,101 +1,101 @@
|
||||
No filters,test data
|
||||
4244.309,3423.133
|
||||
4172.153,3839.874
|
||||
4318.167,3651.161
|
||||
4141.307,3886.542
|
||||
4153.546,3293.166
|
||||
4313.574,3639.47
|
||||
4212.2,3422.614
|
||||
3944.194,3928.898
|
||||
3470.867,3395.562
|
||||
3680.557,4233.545
|
||||
3639.904,3739.869
|
||||
3601.206,4331.278
|
||||
3602.268,3561.573
|
||||
4041.709,3360.442
|
||||
3326.243,3898.576
|
||||
3519.295,3710.73
|
||||
3421.704,3785.601
|
||||
3761.544,3720.579
|
||||
3849.834,3419.051
|
||||
3771.48,3525.297
|
||||
3477.096,3709.462
|
||||
3752.154,3410.653
|
||||
3828.539,3784.068
|
||||
3601.283,4371.022
|
||||
3550.535,3353.485
|
||||
3573.931,4326.953
|
||||
3989.022,3630.239
|
||||
3758.771,3187.932
|
||||
3764.081,3348.153
|
||||
3552.11,3210.788
|
||||
3624.703,3580.683
|
||||
3495.138,3702.232
|
||||
3679.786,3211.763
|
||||
3965.941,4386.728
|
||||
3481.692,4312.93
|
||||
3472.266,3638.52
|
||||
3902.087,4356.89
|
||||
4162.868,3770.82
|
||||
3556.674,3899.06
|
||||
3568.287,3768.694
|
||||
3813.52,3794.494
|
||||
3538.6,4233.813
|
||||
3583.165,3598.301
|
||||
3545.668,3574.602
|
||||
3498.538,3731.551
|
||||
4069.232,3732.176
|
||||
3488.875,4390.112
|
||||
3471.224,4308.19
|
||||
3487.893,3713.36
|
||||
3556.706,3783.748
|
||||
4134.049,4075.267
|
||||
3619.571,3616.779
|
||||
3880.411,4017.523
|
||||
3437.287,4024.127
|
||||
3571.923,4136.496
|
||||
3355.569,4297.359
|
||||
3621.019,3428.405
|
||||
3432.623,3962.733
|
||||
3541.66,3558.748
|
||||
3506.787,3874.117
|
||||
4124.636,3616.127
|
||||
3585.123,3360.593
|
||||
3572.09,3416.381
|
||||
3344.338,3861.743
|
||||
3540.41,3412.915
|
||||
3768.322,3490.888
|
||||
3865.742,3149.312
|
||||
3543.772,3438.211
|
||||
3649.759,3538.124
|
||||
3714.508,3298.845
|
||||
3989.119,3652.572
|
||||
4004.341,3688.486
|
||||
3942.733,3533.375
|
||||
3767.707,3692.636
|
||||
3854.87,3567.363
|
||||
3818.102,4325.471
|
||||
4326.545,3464.113
|
||||
3331.279,3346.4
|
||||
3782.928,3599.129
|
||||
3441.486,3571.214
|
||||
3688.115,3778.354
|
||||
3523.493,4268.157
|
||||
3350.288,3241.872
|
||||
3337.668,3405.69
|
||||
3467.795,3655.209
|
||||
3695.322,3161.427
|
||||
4111.114,3289.313
|
||||
3499.726,3157.723
|
||||
3731.525,3334.048
|
||||
4226.314,3315.567
|
||||
3430.903,3176.271
|
||||
3480.629,3296.73
|
||||
3930.84,3302.929
|
||||
3702.883,3251.164
|
||||
3839.087,3180.461
|
||||
3831.296,3215.8
|
||||
3615.657,3262.533
|
||||
3766.269,3446.736
|
||||
3556.331,4274.897
|
||||
3843.934,3370.384
|
||||
No filters,test data
|
||||
4244.309,3423.133
|
||||
4172.153,3839.874
|
||||
4318.167,3651.161
|
||||
4141.307,3886.542
|
||||
4153.546,3293.166
|
||||
4313.574,3639.47
|
||||
4212.2,3422.614
|
||||
3944.194,3928.898
|
||||
3470.867,3395.562
|
||||
3680.557,4233.545
|
||||
3639.904,3739.869
|
||||
3601.206,4331.278
|
||||
3602.268,3561.573
|
||||
4041.709,3360.442
|
||||
3326.243,3898.576
|
||||
3519.295,3710.73
|
||||
3421.704,3785.601
|
||||
3761.544,3720.579
|
||||
3849.834,3419.051
|
||||
3771.48,3525.297
|
||||
3477.096,3709.462
|
||||
3752.154,3410.653
|
||||
3828.539,3784.068
|
||||
3601.283,4371.022
|
||||
3550.535,3353.485
|
||||
3573.931,4326.953
|
||||
3989.022,3630.239
|
||||
3758.771,3187.932
|
||||
3764.081,3348.153
|
||||
3552.11,3210.788
|
||||
3624.703,3580.683
|
||||
3495.138,3702.232
|
||||
3679.786,3211.763
|
||||
3965.941,4386.728
|
||||
3481.692,4312.93
|
||||
3472.266,3638.52
|
||||
3902.087,4356.89
|
||||
4162.868,3770.82
|
||||
3556.674,3899.06
|
||||
3568.287,3768.694
|
||||
3813.52,3794.494
|
||||
3538.6,4233.813
|
||||
3583.165,3598.301
|
||||
3545.668,3574.602
|
||||
3498.538,3731.551
|
||||
4069.232,3732.176
|
||||
3488.875,4390.112
|
||||
3471.224,4308.19
|
||||
3487.893,3713.36
|
||||
3556.706,3783.748
|
||||
4134.049,4075.267
|
||||
3619.571,3616.779
|
||||
3880.411,4017.523
|
||||
3437.287,4024.127
|
||||
3571.923,4136.496
|
||||
3355.569,4297.359
|
||||
3621.019,3428.405
|
||||
3432.623,3962.733
|
||||
3541.66,3558.748
|
||||
3506.787,3874.117
|
||||
4124.636,3616.127
|
||||
3585.123,3360.593
|
||||
3572.09,3416.381
|
||||
3344.338,3861.743
|
||||
3540.41,3412.915
|
||||
3768.322,3490.888
|
||||
3865.742,3149.312
|
||||
3543.772,3438.211
|
||||
3649.759,3538.124
|
||||
3714.508,3298.845
|
||||
3989.119,3652.572
|
||||
4004.341,3688.486
|
||||
3942.733,3533.375
|
||||
3767.707,3692.636
|
||||
3854.87,3567.363
|
||||
3818.102,4325.471
|
||||
4326.545,3464.113
|
||||
3331.279,3346.4
|
||||
3782.928,3599.129
|
||||
3441.486,3571.214
|
||||
3688.115,3778.354
|
||||
3523.493,4268.157
|
||||
3350.288,3241.872
|
||||
3337.668,3405.69
|
||||
3467.795,3655.209
|
||||
3695.322,3161.427
|
||||
4111.114,3289.313
|
||||
3499.726,3157.723
|
||||
3731.525,3334.048
|
||||
4226.314,3315.567
|
||||
3430.903,3176.271
|
||||
3480.629,3296.73
|
||||
3930.84,3302.929
|
||||
3702.883,3251.164
|
||||
3839.087,3180.461
|
||||
3831.296,3215.8
|
||||
3615.657,3262.533
|
||||
3766.269,3446.736
|
||||
3556.331,4274.897
|
||||
3843.934,3370.384
|
||||
|
||||
|
Reference in New Issue
Block a user