Compare commits
17 Commits
88f4f54b55
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c387d9b40b | ||
|
|
d8061985d6 | ||
|
|
c237112077 | ||
|
|
811773e009 | ||
|
|
f1ebada95d | ||
|
|
9af3023a37 | ||
|
|
16f96aa6f6 | ||
|
|
49c6c14fe5 | ||
|
|
b352790f64 | ||
|
|
f3024cc9a8 | ||
|
|
753ed241b6 | ||
|
|
676e1dcb77 | ||
|
|
0c5a681f5b | ||
|
|
c726855b1c | ||
|
|
bb4addf590 | ||
|
|
f554ac558a | ||
|
|
0492f16cea |
420
.github/workflows/docker-image.yml
vendored
420
.github/workflows/docker-image.yml
vendored
@@ -1,210 +1,210 @@
|
||||
name: Create and publish Docker images
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
docker_build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
run_tests: true
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
run_tests: true
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and run firegex
|
||||
if: matrix.run_tests
|
||||
run: python3 run.py start -P testpassword
|
||||
|
||||
- name: Run tests
|
||||
if: matrix.run_tests
|
||||
run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update version in setup.py
|
||||
run: >-
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" backend/utils/__init__.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }}
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
docker_manifest:
|
||||
needs: docker_build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and push multi-platform manifest
|
||||
run: |
|
||||
# Create manifest list for specific tag
|
||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64
|
||||
|
||||
# Annotate the manifest with architecture info
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}
|
||||
|
||||
# Create manifest list for latest tag
|
||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64
|
||||
|
||||
# Annotate the latest manifest with architecture info
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest
|
||||
|
||||
create-rootfs-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [docker_manifest]
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get latest release tag
|
||||
id: get_tag
|
||||
run: |
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')
|
||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Latest release tag: $LATEST_TAG"
|
||||
|
||||
- name: Export rootfs for amd64
|
||||
run: |
|
||||
echo "Creating and exporting amd64 container..."
|
||||
CONTAINER_ID=$(docker create --platform linux/amd64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing amd64 rootfs..."
|
||||
gzip firegex-rootfs-amd64.tar
|
||||
ls -lh firegex-rootfs-amd64.tar.gz
|
||||
|
||||
- name: Export rootfs for arm64
|
||||
run: |
|
||||
echo "Creating and exporting arm64 container..."
|
||||
CONTAINER_ID=$(docker create --platform linux/arm64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing arm64 rootfs..."
|
||||
gzip firegex-rootfs-arm64.tar
|
||||
ls -lh firegex-rootfs-arm64.tar.gz
|
||||
|
||||
- name: Upload rootfs assets to release
|
||||
run: |
|
||||
echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..."
|
||||
gh release upload ${{ steps.get_tag.outputs.tag }} \
|
||||
firegex-rootfs-amd64.tar.gz \
|
||||
firegex-rootfs-arm64.tar.gz \
|
||||
--clobber
|
||||
echo "Assets uploaded successfully!"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: Create and publish Docker images
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
docker_build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
arch: amd64
|
||||
run_tests: true
|
||||
- os: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
run_tests: true
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and run firegex
|
||||
if: matrix.run_tests
|
||||
run: python3 run.py start -P testpassword
|
||||
|
||||
- name: Run tests
|
||||
if: matrix.run_tests
|
||||
run: sudo apt-get install -y iperf3 && cd tests && ./run_tests.sh
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update version in setup.py
|
||||
run: >-
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" backend/utils/__init__.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-${{ matrix.arch }}
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-${{ matrix.arch }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
docker_manifest:
|
||||
needs: docker_build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create and push multi-platform manifest
|
||||
run: |
|
||||
# Create manifest list for specific tag
|
||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64
|
||||
|
||||
# Annotate the manifest with architecture info
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-amd64 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }} \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}-arm64 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.tag.outputs.TAG_NAME }}
|
||||
|
||||
# Create manifest list for latest tag
|
||||
docker manifest create ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||
--amend ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64
|
||||
|
||||
# Annotate the latest manifest with architecture info
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-amd64 \
|
||||
--arch amd64 --os linux
|
||||
|
||||
docker manifest annotate ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest \
|
||||
${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest-arm64 \
|
||||
--arch arm64 --os linux
|
||||
|
||||
docker manifest push ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:latest
|
||||
|
||||
create-rootfs-assets:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [docker_manifest]
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Convert repository name to lowercase
|
||||
id: lowercase
|
||||
run: echo "image_name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@master
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@master
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Get latest release tag
|
||||
id: get_tag
|
||||
run: |
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')
|
||||
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Latest release tag: $LATEST_TAG"
|
||||
|
||||
- name: Export rootfs for amd64
|
||||
run: |
|
||||
echo "Creating and exporting amd64 container..."
|
||||
CONTAINER_ID=$(docker create --platform linux/amd64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-amd64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing amd64 rootfs..."
|
||||
gzip firegex-rootfs-amd64.tar
|
||||
ls -lh firegex-rootfs-amd64.tar.gz
|
||||
|
||||
- name: Export rootfs for arm64
|
||||
run: |
|
||||
echo "Creating and exporting arm64 container..."
|
||||
CONTAINER_ID=$(docker create --platform linux/arm64 ${{ env.REGISTRY }}/${{ steps.lowercase.outputs.image_name }}:${{ steps.get_tag.outputs.tag }})
|
||||
docker export $CONTAINER_ID --output="firegex-rootfs-arm64.tar"
|
||||
docker rm $CONTAINER_ID
|
||||
echo "Compressing arm64 rootfs..."
|
||||
gzip firegex-rootfs-arm64.tar
|
||||
ls -lh firegex-rootfs-arm64.tar.gz
|
||||
|
||||
- name: Upload rootfs assets to release
|
||||
run: |
|
||||
echo "Uploading assets to release ${{ steps.get_tag.outputs.tag }}..."
|
||||
gh release upload ${{ steps.get_tag.outputs.tag }} \
|
||||
firegex-rootfs-amd64.tar.gz \
|
||||
firegex-rootfs-arm64.tar.gz \
|
||||
--clobber
|
||||
echo "Assets uploaded successfully!"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
92
.github/workflows/pypi-publish-fgex.yml
vendored
92
.github/workflows/pypi-publish-fgex.yml
vendored
@@ -1,46 +1,46 @@
|
||||
# This workflow will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Upload Python Package (fgex alias)
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
- name: Update version in setup.py
|
||||
run: >-
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
||||
- name: Build package
|
||||
run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN_FGEX }}
|
||||
# # This workflow will upload a Python Package using Twine when a release is created
|
||||
# # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# # This workflow uses actions that are not certified by GitHub.
|
||||
# # They are provided by a third-party and are governed by
|
||||
# # separate terms of service, privacy policy, and support
|
||||
# # documentation.
|
||||
|
||||
# name: Upload Python Package (fgex alias)
|
||||
|
||||
# on:
|
||||
# release:
|
||||
# types:
|
||||
# - published
|
||||
|
||||
# permissions:
|
||||
# contents: read
|
||||
|
||||
# jobs:
|
||||
# deploy:
|
||||
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: '3.x'
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip
|
||||
# pip install build
|
||||
# - name: Extract tag name
|
||||
# id: tag
|
||||
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
# - name: Update version in setup.py
|
||||
# run: >-
|
||||
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/fgex-pip/setup.py;
|
||||
# - name: Build package
|
||||
# run: cd fgex-lib/fgex-pip && python -m build && mv ./dist ../../
|
||||
# - name: Publish package
|
||||
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
# with:
|
||||
# user: __token__
|
||||
# password: ${{ secrets.PYPI_API_TOKEN_FGEX }}
|
||||
|
||||
94
.github/workflows/pypi-publish.yml
vendored
94
.github/workflows/pypi-publish.yml
vendored
@@ -1,47 +1,47 @@
|
||||
# This workflow will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Upload Python Package
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
- name: Extract tag name
|
||||
id: tag
|
||||
run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
- name: Update version in setup.py
|
||||
run: >-
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||
sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
- name: Build package
|
||||
run: cd fgex-lib && python -m build && mv ./dist ../
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
# # This workflow will upload a Python Package using Twine when a release is created
|
||||
# # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# # This workflow uses actions that are not certified by GitHub.
|
||||
# # They are provided by a third-party and are governed by
|
||||
# # separate terms of service, privacy policy, and support
|
||||
# # documentation.
|
||||
|
||||
# name: Upload Python Package
|
||||
|
||||
# on:
|
||||
# release:
|
||||
# types:
|
||||
# - published
|
||||
|
||||
# permissions:
|
||||
# contents: read
|
||||
|
||||
# jobs:
|
||||
# deploy:
|
||||
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: '3.x'
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip
|
||||
# pip install build
|
||||
# - name: Extract tag name
|
||||
# id: tag
|
||||
# run: echo TAG_NAME=$(echo $GITHUB_REF | cut -d / -f 3) >> $GITHUB_OUTPUT
|
||||
# - name: Update version in setup.py
|
||||
# run: >-
|
||||
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/setup.py;
|
||||
# sed -i "s/{{VERSION_PLACEHOLDER}}/${{ steps.tag.outputs.TAG_NAME }}/g" fgex-lib/firegex/__init__.py;
|
||||
# - name: Build package
|
||||
# run: cd fgex-lib && python -m build && mv ./dist ../
|
||||
# - name: Publish package
|
||||
# uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
|
||||
# with:
|
||||
# user: __token__
|
||||
# password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
|
||||
112
Dockerfile
112
Dockerfile
@@ -1,49 +1,63 @@
|
||||
|
||||
# Firegex Dockerfile UUID signature
|
||||
# cf1795af-3284-4183-a888-81ad3590ad84
|
||||
# Needed for run.py to detect the Dockerfile
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM oven/bun AS frontend
|
||||
WORKDIR /app
|
||||
ADD ./frontend/package.json .
|
||||
ADD ./frontend/bun.lock .
|
||||
RUN bun i
|
||||
COPY ./frontend/ .
|
||||
RUN bun run build
|
||||
|
||||
# Base fedora container
|
||||
FROM --platform=$TARGETARCH quay.io/fedora/fedora:42 AS base
|
||||
RUN dnf -y update && dnf install -y python3.13 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.13-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.13 -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.13-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.13-devel uv git && dnf clean all
|
||||
|
||||
COPY ./backend/ /execute/
|
||||
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
|
||||
COPY --from=frontend /app/dist/ ./frontend/
|
||||
|
||||
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"]
|
||||
|
||||
# Firegex Dockerfile UUID signature
|
||||
# cf1795af-3284-4183-a888-81ad3590ad84
|
||||
# Needed for run.py to detect the Dockerfile
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM oven/bun AS frontend
|
||||
WORKDIR /app
|
||||
ADD ./frontend/package.json .
|
||||
ADD ./frontend/bun.lock .
|
||||
RUN bun i
|
||||
COPY ./frontend/ .
|
||||
RUN bun run build
|
||||
|
||||
# Base Ubuntu container
|
||||
FROM --platform=$TARGETARCH ubuntu:24.04 AS base
|
||||
RUN apt-get update && apt-get install -y python3 libnetfilter-queue1 \
|
||||
libnfnetlink0 libmnl0 libcap-ng-utils libcap2-bin nftables \
|
||||
libhyperscan5 python3-nftables libpcap0.8 && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /execute/modules
|
||||
WORKDIR /execute
|
||||
|
||||
FROM --platform=$TARGETARCH base AS compiler
|
||||
|
||||
RUN apt-get update && apt-get install -y python3-dev build-essential g++ \
|
||||
libnetfilter-queue-dev libnfnetlink-dev libmnl-dev \
|
||||
libhyperscan-dev libpcap-dev libboost-dev pkg-config wget cmake && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Build libtins from source as it's not available in Ubuntu 24.04
|
||||
RUN wget https://github.com/mfontanini/libtins/archive/v4.5.tar.gz && \
|
||||
tar -xzf v4.5.tar.gz && cd libtins-4.5 && \
|
||||
mkdir build && cd build && \
|
||||
cmake ../ -DLIBTINS_ENABLE_CXX11=1 && \
|
||||
make && make install && ldconfig && \
|
||||
cd ../.. && rm -rf libtins-4.5 v4.5.tar.gz
|
||||
|
||||
COPY ./backend/binsrc /execute/binsrc
|
||||
RUN g++ binsrc/nfregex.cpp -o cppregex -std=c++23 -O3 -lnetfilter_queue -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libhs libmnl)
|
||||
RUN g++ binsrc/nfproxy.cpp -o cpproxy -std=c++23 -O3 -lnetfilter_queue -lpython3.12 -pthread -lnfnetlink $(pkg-config --cflags --libs libtins libmnl python3)
|
||||
|
||||
#Building main conteiner
|
||||
FROM --platform=$TARGETARCH base AS final
|
||||
|
||||
COPY ./backend/requirements.txt /execute/requirements.txt
|
||||
COPY ./fgex-lib /execute/fgex-lib
|
||||
|
||||
RUN apt-get update && apt-get install -y g++ python3-dev python3-pip git && \
|
||||
pip3 install --no-cache-dir --break-system-packages ./fgex-lib && \
|
||||
pip3 install --no-cache-dir --break-system-packages -r /execute/requirements.txt && \
|
||||
apt-get remove -y g++ python3-dev git && \
|
||||
apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY ./backend/ /execute/
|
||||
COPY --from=compiler /execute/cppregex /execute/cpproxy /execute/modules/
|
||||
COPY --from=compiler /usr/local/lib/libtins* /usr/local/lib/
|
||||
COPY --from=frontend /app/dist/ ./frontend/
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
CMD ["/bin/sh", "/execute/docker-entrypoint.sh"]
|
||||
|
||||
31
README.md
31
README.md
@@ -68,6 +68,37 @@ All the configuration at the startup is customizable in [firegex.py](./run.py) o
|
||||
- Create basic firewall rules to allow and deny specific traffic, like ufw or iptables but using firegex graphic interface (by using [nftable](https://netfilter.org/projects/nftables/))
|
||||
- Port Hijacking allows you to redirect the traffic on a specific port to another port. Thanks to this you can start your own proxy, connecting to the real service using the loopback interface. Firegex will be resposable about the routing of the packets using internally [nftables](https://netfilter.org/projects/nftables/)
|
||||
- EXPERIMENTAL: Netfilter Proxy uses [nfqueue](https://netfilter.org/projects/libnetfilter_queue/) to simulate a python proxy, you can write your own filter in python and use it to filter the traffic. There are built-in some data handler to parse protocols like HTTP, and before apply the filter you can test it with fgex command (you need to install firegex lib from pypi).
|
||||
- Traffic Viewer allows you to monitor live network traffic for all services in real-time
|
||||
- Setup Import/Export allows you to backup and restore your entire Firegex configuration as a JSON file, making it easy to deploy identical configurations across multiple servers
|
||||
|
||||
## Configuration Management
|
||||
|
||||
Firegex supports importing and exporting configurations via JSON files. This is useful for:
|
||||
- Backing up your configuration
|
||||
- Deploying the same setup across multiple servers
|
||||
- Version controlling your firewall rules
|
||||
- Quick disaster recovery
|
||||
|
||||
### Using the Web Interface
|
||||
Navigate to "Setup Import/Export" in the sidebar to:
|
||||
- **Export**: Download your current configuration as JSON
|
||||
- **Import from File**: Upload a setup.json file
|
||||
- **Import from JSON**: Paste JSON directly into the interface
|
||||
|
||||
### Using the API
|
||||
```bash
|
||||
# Export configuration
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
http://localhost:4444/api/setup/export > setup.json
|
||||
|
||||
# Import configuration
|
||||
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @setup.json \
|
||||
http://localhost:4444/api/setup/import
|
||||
```
|
||||
|
||||
See [setup.example.json](setup.example.json) for the configuration file format.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ if __name__ == '__main__':
|
||||
uvicorn.run(
|
||||
"app:app",
|
||||
# None allows to bind also on ipv6, and is selected if FIREGEX_HOST is any
|
||||
host=None if FIREGEX_HOST == "any" else FIREGEX_HOST,
|
||||
host="" if FIREGEX_HOST == "any" else FIREGEX_HOST,
|
||||
port=FIREGEX_PORT,
|
||||
uds=FIREGEX_SOCKET,
|
||||
reload=DEBUG and not NORELOAD,
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
chown nobody -R /execute/
|
||||
|
||||
# Create socket directory if SOCKET_DIR is set
|
||||
if [ -n "$SOCKET_DIR" ]; then
|
||||
mkdir -p "$SOCKET_DIR"
|
||||
chown nobody:nobody "$SOCKET_DIR"
|
||||
chmod 755 "$SOCKET_DIR"
|
||||
fi
|
||||
|
||||
echo "[*] Attempting to start with capabilities..."
|
||||
|
||||
if capsh --caps="cap_net_admin,cap_setpcap,cap_setuid,cap_setgid,cap_sys_nice+eip" \
|
||||
|
||||
@@ -5,6 +5,7 @@ import asyncio
|
||||
import traceback
|
||||
from fastapi import HTTPException
|
||||
import time
|
||||
import json
|
||||
from utils import run_func
|
||||
from utils import DEBUG
|
||||
from utils import nicenessify
|
||||
@@ -35,11 +36,12 @@ class FiregexInterceptor:
|
||||
self.last_time_exception = 0
|
||||
self.outstrem_function = None
|
||||
self.expection_function = None
|
||||
self.traffic_function = None
|
||||
self.outstrem_task: asyncio.Task
|
||||
self.outstrem_buffer = ""
|
||||
|
||||
@classmethod
|
||||
async def start(cls, srv: Service, outstream_func=None, exception_func=None):
|
||||
async def start(cls, srv: Service, outstream_func=None, exception_func=None, traffic_func=None):
|
||||
self = cls()
|
||||
self.srv = srv
|
||||
self.filter_map_lock = asyncio.Lock()
|
||||
@@ -47,6 +49,7 @@ class FiregexInterceptor:
|
||||
self.sock_conn_lock = asyncio.Lock()
|
||||
self.outstrem_function = outstream_func
|
||||
self.expection_function = exception_func
|
||||
self.traffic_function = traffic_func
|
||||
if not self.sock_conn_lock.locked():
|
||||
await self.sock_conn_lock.acquire()
|
||||
self.sock_path = f"/tmp/firegex_nfproxy_{srv.id}.sock"
|
||||
@@ -83,9 +86,21 @@ class FiregexInterceptor:
|
||||
self.outstrem_buffer = self.outstrem_buffer[-OUTSTREAM_BUFFER_SIZE:]+"\n"
|
||||
if self.outstrem_function:
|
||||
await run_func(self.outstrem_function, self.srv.id, out_data)
|
||||
# Parse JSON traffic events (if binary emits them)
|
||||
if self.traffic_function:
|
||||
for line in out_data.splitlines():
|
||||
if line.startswith("{"): # JSON event from binary
|
||||
try:
|
||||
event = json.loads(line)
|
||||
if "ts" in event and "verdict" in event: # Basic validation
|
||||
await run_func(self.traffic_function, self.srv.id, event)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
pass # Ignore malformed JSON, keep backward compat with raw logs
|
||||
|
||||
async def _start_binary(self):
|
||||
proxy_binary_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cpproxy"))
|
||||
# Determine match mode based on protocol
|
||||
match_mode = "stream" if self.srv.proto in ["tcp", "http"] else "block"
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
proxy_binary_path, stdin=asyncio.subprocess.DEVNULL,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
@@ -93,7 +108,9 @@ class FiregexInterceptor:
|
||||
env={
|
||||
"NTHREADS": os.getenv("NTHREADS","1"),
|
||||
"FIREGEX_NFQUEUE_FAIL_OPEN": "1" if self.srv.fail_open else "0",
|
||||
"FIREGEX_NFPROXY_SOCK": self.sock_path
|
||||
"FIREGEX_NFPROXY_SOCK": self.sock_path,
|
||||
"MATCH_MODE": match_mode,
|
||||
"PROTOCOL": self.srv.proto
|
||||
},
|
||||
)
|
||||
nicenessify(-10, self.process.pid)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from modules.nfproxy.firegex import FiregexInterceptor
|
||||
from modules.nfproxy.nftables import FiregexTables, FiregexFilter
|
||||
from modules.nfproxy.models import Service, PyFilter
|
||||
@@ -12,7 +13,7 @@ class STATUS:
|
||||
nft = FiregexTables()
|
||||
|
||||
class ServiceManager:
|
||||
def __init__(self, srv: Service, db, outstream_func=None, exception_func=None):
|
||||
def __init__(self, srv: Service, db, outstream_func=None, exception_func=None, traffic_func=None):
|
||||
self.srv = srv
|
||||
self.db = db
|
||||
self.status = STATUS.STOP
|
||||
@@ -21,11 +22,17 @@ class ServiceManager:
|
||||
self.interceptor = None
|
||||
self.outstream_function = outstream_func
|
||||
self.last_exception_time = 0
|
||||
self.traffic_events = deque(maxlen=500) # Ring buffer for traffic viewer
|
||||
async def excep_internal_handler(srv, exc_time):
|
||||
self.last_exception_time = exc_time
|
||||
if exception_func:
|
||||
await run_func(exception_func, srv, exc_time)
|
||||
self.exception_function = excep_internal_handler
|
||||
async def traffic_internal_handler(srv, event):
|
||||
self.traffic_events.append(event)
|
||||
if traffic_func:
|
||||
await run_func(traffic_func, srv, event)
|
||||
self.traffic_function = traffic_internal_handler
|
||||
|
||||
async def _update_filters_from_db(self):
|
||||
pyfilters = [
|
||||
@@ -69,7 +76,7 @@ class ServiceManager:
|
||||
async def start(self):
|
||||
if not self.interceptor:
|
||||
nft.delete(self.srv)
|
||||
self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
||||
self.interceptor = await FiregexInterceptor.start(self.srv, outstream_func=self.outstream_function, exception_func=self.exception_function, traffic_func=self.traffic_function)
|
||||
await self._update_filters_from_db()
|
||||
self._set_status(STATUS.ACTIVE)
|
||||
|
||||
@@ -87,14 +94,24 @@ class ServiceManager:
|
||||
async def update_filters(self):
|
||||
async with self.lock:
|
||||
await self._update_filters_from_db()
|
||||
|
||||
def get_traffic_events(self, limit: int = 500):
|
||||
"""Return recent traffic events from ring buffer"""
|
||||
events_list = list(self.traffic_events)
|
||||
return events_list[-limit:] if limit < len(events_list) else events_list
|
||||
|
||||
def clear_traffic_events(self):
|
||||
"""Clear traffic event history"""
|
||||
self.traffic_events.clear()
|
||||
|
||||
class FirewallManager:
|
||||
def __init__(self, db:SQLite, outstream_func=None, exception_func=None):
|
||||
def __init__(self, db:SQLite, outstream_func=None, exception_func=None, traffic_func=None):
|
||||
self.db = db
|
||||
self.service_table: dict[str, ServiceManager] = {}
|
||||
self.lock = asyncio.Lock()
|
||||
self.outstream_function = outstream_func
|
||||
self.exception_function = exception_func
|
||||
self.traffic_function = traffic_func
|
||||
|
||||
async def close(self):
|
||||
for key in list(self.service_table.keys()):
|
||||
@@ -116,7 +133,7 @@ class FirewallManager:
|
||||
srv = Service.from_dict(srv)
|
||||
if srv.id in self.service_table:
|
||||
continue
|
||||
self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function)
|
||||
self.service_table[srv.id] = ServiceManager(srv, self.db, outstream_func=self.outstream_function, exception_func=self.exception_function, traffic_func=self.traffic_function)
|
||||
await self.service_table[srv.id].next(srv.status)
|
||||
|
||||
def get(self,srv_id) -> ServiceManager:
|
||||
|
||||
@@ -6,6 +6,8 @@ def convert_protocol_to_l4(proto:str):
|
||||
return "tcp"
|
||||
elif proto == "http":
|
||||
return "tcp"
|
||||
elif proto == "udp":
|
||||
return "udp"
|
||||
else:
|
||||
raise Exception("Invalid protocol")
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
fastapi[all]
|
||||
httpx
|
||||
uvicorn[standard]
|
||||
psutil
|
||||
python-jose[cryptography]
|
||||
python-socketio
|
||||
git+https://github.com/google/brotli.git@35d4992ac8eb1eca3b6c5f220e76cfc8b7e470aa
|
||||
#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py
|
||||
fastapi[all]
|
||||
httpx
|
||||
uvicorn[standard]
|
||||
psutil
|
||||
python-jose[cryptography]
|
||||
python-socketio
|
||||
brotli
|
||||
zstandard
|
||||
#git+https://salsa.debian.org/pkg-netfilter-team/pkg-nftables#egg=nftables&subdirectory=py
|
||||
|
||||
@@ -65,7 +65,7 @@ db = SQLite('db/nft-pyfilters.db', {
|
||||
'status': 'VARCHAR(100) NOT NULL',
|
||||
'port': 'INT NOT NULL CHECK(port > 0 and port < 65536)',
|
||||
'name': 'VARCHAR(100) NOT NULL UNIQUE',
|
||||
'proto': 'VARCHAR(3) NOT NULL CHECK (proto IN ("tcp", "http"))',
|
||||
'proto': 'VARCHAR(4) NOT NULL CHECK (proto IN ("tcp", "http", "udp"))',
|
||||
'l4_proto': 'VARCHAR(3) NOT NULL CHECK (l4_proto IN ("tcp", "udp"))',
|
||||
'ip_int': 'VARCHAR(100) NOT NULL',
|
||||
'fail_open': 'BOOLEAN NOT NULL CHECK (fail_open IN (0, 1)) DEFAULT 1',
|
||||
@@ -113,6 +113,8 @@ async def startup():
|
||||
utils.socketio.on("nfproxy-outstream-leave", leave_outstream)
|
||||
utils.socketio.on("nfproxy-exception-join", join_exception)
|
||||
utils.socketio.on("nfproxy-exception-leave", leave_exception)
|
||||
utils.socketio.on("nfproxy-traffic-join", join_traffic)
|
||||
utils.socketio.on("nfproxy-traffic-leave", leave_traffic)
|
||||
|
||||
async def shutdown():
|
||||
db.backup()
|
||||
@@ -133,7 +135,10 @@ async def outstream_func(service_id, data):
|
||||
async def exception_func(service_id, timestamp):
|
||||
await utils.socketio.emit(f"nfproxy-exception-{service_id}", timestamp, room=f"nfproxy-exception-{service_id}")
|
||||
|
||||
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func)
|
||||
async def traffic_func(service_id, event):
|
||||
await utils.socketio.emit(f"nfproxy-traffic-{service_id}", event, room=f"nfproxy-traffic-{service_id}")
|
||||
|
||||
firewall = FirewallManager(db, outstream_func=outstream_func, exception_func=exception_func, traffic_func=traffic_func)
|
||||
|
||||
@app.get('/services', response_model=list[ServiceModel])
|
||||
async def get_service_list():
|
||||
@@ -300,7 +305,7 @@ async def add_new_service(form: ServiceAddForm):
|
||||
form.ip_int = ip_parse(form.ip_int)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid address")
|
||||
if form.proto not in ["tcp", "http"]:
|
||||
if form.proto not in ["tcp", "http", "udp"]:
|
||||
raise HTTPException(status_code=400, detail="Invalid protocol")
|
||||
srv_id = None
|
||||
try:
|
||||
@@ -368,6 +373,28 @@ async def get_pyfilters_code(service_id: str):
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
|
||||
@app.get('/services/{service_id}/traffic')
|
||||
async def get_traffic_events(service_id: str, limit: int = 500):
|
||||
"""Get recent traffic events from the service ring buffer"""
|
||||
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
|
||||
raise HTTPException(status_code=400, detail="This service does not exists!")
|
||||
try:
|
||||
events = firewall.get(service_id).get_traffic_events(limit)
|
||||
return {"events": events, "count": len(events)}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post('/services/{service_id}/traffic/clear', response_model=StatusMessageModel)
|
||||
async def clear_traffic_events(service_id: str):
|
||||
"""Clear traffic event history for a service"""
|
||||
if not db.query("SELECT 1 FROM services WHERE service_id = ?;", service_id):
|
||||
raise HTTPException(status_code=400, detail="This service does not exists!")
|
||||
try:
|
||||
firewall.get(service_id).clear_traffic_events()
|
||||
return {"status": "ok"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
#Socket io events
|
||||
async def join_outstream(sid, data):
|
||||
"""Client joins a room."""
|
||||
@@ -397,3 +424,20 @@ async def leave_exception(sid, data):
|
||||
if srv:
|
||||
await utils.socketio.leave_room(sid, f"nfproxy-exception-{srv}")
|
||||
|
||||
async def join_traffic(sid, data):
|
||||
"""Client joins traffic viewer room and gets initial event history."""
|
||||
srv = data.get("service")
|
||||
if srv:
|
||||
room = f"nfproxy-traffic-{srv}"
|
||||
await utils.socketio.enter_room(sid, room)
|
||||
try:
|
||||
events = firewall.get(srv).get_traffic_events(500)
|
||||
await utils.socketio.emit("nfproxy-traffic-history", {"events": events}, room=sid)
|
||||
except Exception:
|
||||
pass # Service may not exist or not started
|
||||
|
||||
async def leave_traffic(sid, data):
|
||||
"""Client leaves traffic viewer room."""
|
||||
srv = data.get("service")
|
||||
if srv:
|
||||
await utils.socketio.leave_room(sid, f"nfproxy-traffic-{srv}")
|
||||
|
||||
223
backend/routers/setup.py
Normal file
223
backend/routers/setup.py
Normal file
@@ -0,0 +1,223 @@
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
from typing import List, Optional
|
||||
from utils.models import StatusMessageModel
|
||||
from routers import nfproxy, nfregex, porthijack, firewall
|
||||
|
||||
class ServiceConfig(BaseModel):
|
||||
name: str
|
||||
port: int
|
||||
proto: str
|
||||
ip_int: str
|
||||
fail_open: bool = True
|
||||
|
||||
class PortHijackServiceConfig(BaseModel):
|
||||
name: str
|
||||
public_port: int
|
||||
proxy_port: int
|
||||
proto: str
|
||||
ip_src: str
|
||||
ip_dst: str
|
||||
|
||||
class FirewallRuleConfig(BaseModel):
|
||||
mode: str
|
||||
src: str
|
||||
dst: str
|
||||
in_int: str
|
||||
out_int: str
|
||||
proto: str
|
||||
sport: str
|
||||
dport: str
|
||||
|
||||
class SetupConfig(BaseModel):
|
||||
services: Optional[List[ServiceConfig]] = []
|
||||
porthijack: Optional[List[PortHijackServiceConfig]] = []
|
||||
firewall: Optional[List[FirewallRuleConfig]] = []
|
||||
|
||||
class SetupResponse(BaseModel):
|
||||
status: str
|
||||
services_created: int = 0
|
||||
porthijack_created: int = 0
|
||||
firewall_created: int = 0
|
||||
errors: List[str] = []
|
||||
|
||||
app = APIRouter()
|
||||
|
||||
@app.post("/import", response_model=SetupResponse)
|
||||
async def import_setup(config: SetupConfig):
|
||||
"""
|
||||
Import services and rules from a setup configuration.
|
||||
Creates basic services without filters or regex rules.
|
||||
"""
|
||||
errors = []
|
||||
services_count = 0
|
||||
porthijack_count = 0
|
||||
firewall_count = 0
|
||||
|
||||
# Import Services
|
||||
if config.services:
|
||||
for service_config in config.services:
|
||||
try:
|
||||
# Determine which module to use based on protocol
|
||||
# HTTP -> NFProxy, TCP/UDP -> can use either (prefer NFProxy)
|
||||
if service_config.proto in ["tcp", "http", "udp"]:
|
||||
# Create NFProxy service
|
||||
try:
|
||||
add_form = nfproxy.ServiceAddForm(
|
||||
name=service_config.name,
|
||||
port=service_config.port,
|
||||
proto=service_config.proto,
|
||||
ip_int=service_config.ip_int,
|
||||
fail_open=service_config.fail_open
|
||||
)
|
||||
result = await nfproxy.add_service(add_form)
|
||||
|
||||
if result.status == "ok":
|
||||
services_count += 1
|
||||
else:
|
||||
errors.append(f"Service '{service_config.name}': Failed to create")
|
||||
except Exception as e:
|
||||
errors.append(f"Service '{service_config.name}': {str(e)}")
|
||||
else:
|
||||
errors.append(f"Service '{service_config.name}': Unsupported protocol '{service_config.proto}'")
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Service '{service_config.name}': {str(e)}")
|
||||
|
||||
# Import PortHijack services
|
||||
if config.porthijack:
|
||||
for service_config in config.porthijack:
|
||||
try:
|
||||
add_form = porthijack.ServiceAddForm(
|
||||
name=service_config.name,
|
||||
public_port=service_config.public_port,
|
||||
proxy_port=service_config.proxy_port,
|
||||
proto=service_config.proto,
|
||||
ip_src=service_config.ip_src,
|
||||
ip_dst=service_config.ip_dst
|
||||
)
|
||||
result = await porthijack.add_service(add_form)
|
||||
|
||||
if result.status == "ok":
|
||||
porthijack_count += 1
|
||||
else:
|
||||
errors.append(f"PortHijack service '{service_config.name}': Failed to create")
|
||||
except Exception as e:
|
||||
errors.append(f"PortHijack service '{service_config.name}': {str(e)}")
|
||||
|
||||
# Import Firewall rules
|
||||
if config.firewall:
|
||||
for rule_config in config.firewall:
|
||||
try:
|
||||
rule_form = firewall.RuleFormAdd(
|
||||
mode=rule_config.mode,
|
||||
src=rule_config.src,
|
||||
dst=rule_config.dst,
|
||||
in_int=rule_config.in_int,
|
||||
out_int=rule_config.out_int,
|
||||
proto=rule_config.proto,
|
||||
sport=rule_config.sport,
|
||||
dport=rule_config.dport
|
||||
)
|
||||
await firewall.add_rule(rule_form)
|
||||
firewall_count += 1
|
||||
except Exception as e:
|
||||
errors.append(f"Firewall rule: {str(e)}")
|
||||
|
||||
return SetupResponse(
|
||||
status="ok" if len(errors) == 0 else "partial",
|
||||
services_created=services_count,
|
||||
porthijack_created=porthijack_count,
|
||||
firewall_created=firewall_count,
|
||||
errors=errors
|
||||
)
|
||||
|
||||
@app.post("/import/file")
|
||||
async def import_setup_file(file: UploadFile = File(...)):
|
||||
"""
|
||||
Import services from an uploaded JSON file.
|
||||
"""
|
||||
try:
|
||||
content = await file.read()
|
||||
config_dict = json.loads(content.decode('utf-8'))
|
||||
config = SetupConfig(**config_dict)
|
||||
return await import_setup(config)
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid JSON: {str(e)}")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Error processing file: {str(e)}")
|
||||
|
||||
@app.get("/export")
|
||||
async def export_setup():
|
||||
"""
|
||||
Export all current services and rules as a JSON configuration.
|
||||
Exports only service definitions without filters or regexes.
|
||||
"""
|
||||
config = {
|
||||
"services": [],
|
||||
"porthijack": [],
|
||||
"firewall": []
|
||||
}
|
||||
|
||||
# Export NFProxy services
|
||||
try:
|
||||
nfproxy_services = await nfproxy.get_services()
|
||||
for service in nfproxy_services:
|
||||
config["services"].append({
|
||||
"name": service.name,
|
||||
"port": service.port,
|
||||
"proto": service.proto,
|
||||
"ip_int": service.ip_int,
|
||||
"fail_open": service.fail_open
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
# Export NFRegex services
|
||||
try:
|
||||
nfregex_services = await nfregex.get_services()
|
||||
for service in nfregex_services:
|
||||
config["services"].append({
|
||||
"name": service.name,
|
||||
"port": service.port,
|
||||
"proto": service.proto,
|
||||
"ip_int": service.ip_int,
|
||||
"fail_open": service.fail_open
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
# Export PortHijack services
|
||||
try:
|
||||
porthijack_services = await porthijack.get_services()
|
||||
for service in porthijack_services:
|
||||
config["porthijack"].append({
|
||||
"name": service.name,
|
||||
"public_port": service.public_port,
|
||||
"proxy_port": service.proxy_port,
|
||||
"proto": service.proto,
|
||||
"ip_src": service.ip_src,
|
||||
"ip_dst": service.ip_dst
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
# Export Firewall rules
|
||||
try:
|
||||
fw_rules = await firewall.get_rules()
|
||||
for rule in fw_rules:
|
||||
config["firewall"].append({
|
||||
"mode": rule.mode,
|
||||
"src": rule.src,
|
||||
"dst": rule.dst,
|
||||
"in_int": rule.in_int,
|
||||
"out_int": rule.out_int,
|
||||
"proto": rule.proto,
|
||||
"sport": rule.sport,
|
||||
"dport": rule.dport
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
return config
|
||||
@@ -1,221 +1,221 @@
|
||||
import asyncio
|
||||
from ipaddress import ip_address, ip_interface
|
||||
import os
|
||||
import socket
|
||||
import psutil
|
||||
import sys
|
||||
import nftables
|
||||
from socketio import AsyncServer
|
||||
from fastapi import Path
|
||||
from typing import Annotated
|
||||
from functools import wraps
|
||||
from pydantic import BaseModel, ValidationError
|
||||
import traceback
|
||||
from utils.models import StatusMessageModel
|
||||
from typing import List
|
||||
|
||||
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
|
||||
|
||||
socketio:AsyncServer = None
|
||||
sid_list:set = set()
|
||||
|
||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
|
||||
ON_DOCKER = "DOCKER" in sys.argv
|
||||
DEBUG = "DEBUG" in sys.argv
|
||||
NORELOAD = "NORELOAD" in sys.argv
|
||||
FIREGEX_PORT = int(os.getenv("PORT","4444"))
|
||||
FIREGEX_HOST = os.getenv("HOST","0.0.0.0")
|
||||
FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None)
|
||||
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
|
||||
|
||||
PortType = Annotated[int, Path(gt=0, lt=65536)]
|
||||
|
||||
async def run_func(func, *args, **kwargs):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
async def socketio_emit(elements:list[str]):
|
||||
await socketio.emit("update",elements)
|
||||
|
||||
def refactor_name(name:str):
|
||||
name = name.strip()
|
||||
while " " in name:
|
||||
name = name.replace(" "," ")
|
||||
return name
|
||||
|
||||
class SysctlManager:
|
||||
def __init__(self, ctl_table):
|
||||
self.old_table = {}
|
||||
self.new_table = {}
|
||||
if os.path.isdir("/sys_host/"):
|
||||
self.old_table = dict()
|
||||
self.new_table = dict(ctl_table)
|
||||
for name in ctl_table.keys():
|
||||
self.old_table[name] = read_sysctl(name)
|
||||
|
||||
def write_table(self, table) -> bool:
|
||||
for name, value in table.items():
|
||||
if read_sysctl(name) != value:
|
||||
write_sysctl(name, value)
|
||||
|
||||
def set(self):
|
||||
self.write_table(self.new_table)
|
||||
|
||||
def reset(self):
|
||||
self.write_table(self.old_table)
|
||||
|
||||
def read_sysctl(name:str):
|
||||
with open(f"/sys_host/{name}", "rt") as f:
|
||||
return "1" in f.read()
|
||||
|
||||
def write_sysctl(name:str, value:bool):
|
||||
with open(f"/sys_host/{name}", "wt") as f:
|
||||
f.write("1" if value else "0")
|
||||
|
||||
def list_files(mypath):
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
return [f for f in listdir(mypath) if isfile(join(mypath, f))]
|
||||
|
||||
def ip_parse(ip:str):
|
||||
return str(ip_interface(ip).network)
|
||||
|
||||
def is_ip_parse(ip:str):
|
||||
try:
|
||||
ip_parse(ip)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def addr_parse(ip:str):
|
||||
return str(ip_address(ip))
|
||||
|
||||
def ip_family(ip:str):
|
||||
return "ip6" if ip_interface(ip).version == 6 else "ip"
|
||||
|
||||
def get_interfaces():
|
||||
def _get_interfaces():
|
||||
for int_name, interfs in psutil.net_if_addrs().items():
|
||||
for interf in interfs:
|
||||
if interf.family in [socket.AF_INET, socket.AF_INET6]:
|
||||
yield {"name": int_name, "addr":interf.address}
|
||||
return list(_get_interfaces())
|
||||
|
||||
def nftables_int_to_json(ip_int):
|
||||
ip_int = ip_parse(ip_int)
|
||||
ip_addr = str(ip_int).split("/")[0]
|
||||
ip_addr_cidr = int(str(ip_int).split("/")[1])
|
||||
return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}
|
||||
|
||||
def nftables_json_to_int(ip_json_int):
|
||||
if isinstance(ip_json_int,str):
|
||||
return str(ip_parse(ip_json_int))
|
||||
else:
|
||||
return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}'
|
||||
|
||||
class Singleton(object):
|
||||
__instance = None
|
||||
def __new__(class_, *args, **kwargs):
|
||||
if not isinstance(class_.__instance, class_):
|
||||
class_.__instance = object.__new__(class_, *args, **kwargs)
|
||||
return class_.__instance
|
||||
|
||||
class NFTableManager(Singleton):
|
||||
|
||||
table_name = "firegex"
|
||||
|
||||
def __init__(self, init_cmd, reset_cmd):
|
||||
self.__init_cmds = init_cmd
|
||||
self.__reset_cmds = reset_cmd
|
||||
self.nft = nftables.Nftables()
|
||||
|
||||
def raw_cmd(self, *cmds):
|
||||
return self.nft.json_cmd({"nftables": list(cmds)})
|
||||
|
||||
def cmd(self, *cmds):
|
||||
code, out, err = self.raw_cmd(*cmds)
|
||||
if code == 0:
|
||||
return out
|
||||
else:
|
||||
raise Exception(err)
|
||||
|
||||
def init(self):
|
||||
self.reset()
|
||||
self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}})
|
||||
self.cmd(*self.__init_cmds)
|
||||
|
||||
def reset(self):
|
||||
self.raw_cmd(*self.__reset_cmds)
|
||||
|
||||
def list_rules(self, tables = None, chains = None):
|
||||
for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]:
|
||||
if tables and filter["table"] not in tables:
|
||||
continue
|
||||
if chains and filter["chain"] not in chains:
|
||||
continue
|
||||
yield filter
|
||||
|
||||
def raw_list(self):
|
||||
return self.cmd({"list": {"ruleset": None}})["nftables"]
|
||||
|
||||
def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"):
|
||||
res = obj.model_dump(mode=mode, exclude_unset=not unset)
|
||||
if convert_keys:
|
||||
for from_k, to_k in convert_keys.items():
|
||||
if from_k in res:
|
||||
res[to_k] = res.pop(from_k)
|
||||
if exclude:
|
||||
for ele in exclude:
|
||||
if ele in res:
|
||||
del res[ele]
|
||||
return res
|
||||
|
||||
def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict:
|
||||
if isinstance(obj, list):
|
||||
return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj]
|
||||
return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode)
|
||||
|
||||
def register_event(sio_server: AsyncServer, event_name: str, model: BaseModel, response_model: BaseModel|None = None):
|
||||
def decorator(func):
|
||||
@sio_server.on(event_name) # Automatically registers the event
|
||||
@wraps(func)
|
||||
async def wrapper(sid, data):
|
||||
try:
|
||||
# Parse and validate incoming data
|
||||
parsed_data = model.model_validate(data)
|
||||
except ValidationError:
|
||||
return json_like(StatusMessageModel(status=f"Invalid {event_name} request"))
|
||||
|
||||
# Call the original function with the parsed data
|
||||
result = await func(sid, parsed_data)
|
||||
# If a response model is provided, validate the output
|
||||
if response_model:
|
||||
try:
|
||||
parsed_result = response_model.model_validate(result)
|
||||
except ValidationError:
|
||||
traceback.print_exc()
|
||||
return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response"))
|
||||
else:
|
||||
parsed_result = result
|
||||
# Emit the validated result
|
||||
if parsed_result:
|
||||
if isinstance(parsed_result, BaseModel):
|
||||
return json_like(parsed_result)
|
||||
return parsed_result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def nicenessify(priority:int, pid:int|None=None):
|
||||
try:
|
||||
pid = os.getpid() if pid is None else pid
|
||||
ps = psutil.Process(pid)
|
||||
if os.name == 'posix':
|
||||
ps.nice(priority)
|
||||
except Exception as e:
|
||||
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
||||
pass
|
||||
import asyncio
|
||||
from ipaddress import ip_address, ip_interface
|
||||
import os
|
||||
import socket
|
||||
import psutil
|
||||
import sys
|
||||
import nftables
|
||||
from socketio import AsyncServer
|
||||
from fastapi import Path
|
||||
from typing import Annotated
|
||||
from functools import wraps
|
||||
from pydantic import BaseModel, ValidationError
|
||||
import traceback
|
||||
from utils.models import StatusMessageModel
|
||||
from typing import List
|
||||
|
||||
LOCALHOST_IP = socket.gethostbyname(os.getenv("LOCALHOST_IP","127.0.0.1"))
|
||||
|
||||
socketio:AsyncServer = None
|
||||
sid_list:set = set()
|
||||
|
||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
ROUTERS_DIR = os.path.join(ROOT_DIR,"routers")
|
||||
ON_DOCKER = "DOCKER" in sys.argv
|
||||
DEBUG = "DEBUG" in sys.argv
|
||||
NORELOAD = "NORELOAD" in sys.argv
|
||||
FIREGEX_PORT = int(os.getenv("PORT","4444"))
|
||||
FIREGEX_HOST = os.getenv("HOST","0.0.0.0")
|
||||
FIREGEX_SOCKET_DIR = os.getenv("SOCKET_DIR", None)
|
||||
FIREGEX_SOCKET = os.path.join(FIREGEX_SOCKET_DIR, "firegex.sock") if FIREGEX_SOCKET_DIR else None
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
API_VERSION = "{{VERSION_PLACEHOLDER}}" if "{" not in "{{VERSION_PLACEHOLDER}}" else "0.0.0"
|
||||
|
||||
PortType = Annotated[int, Path(gt=0, lt=65536)]
|
||||
|
||||
async def run_func(func, *args, **kwargs):
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
async def socketio_emit(elements:list[str]):
|
||||
await socketio.emit("update",elements)
|
||||
|
||||
def refactor_name(name:str):
|
||||
name = name.strip()
|
||||
while " " in name:
|
||||
name = name.replace(" "," ")
|
||||
return name
|
||||
|
||||
class SysctlManager:
|
||||
def __init__(self, ctl_table):
|
||||
self.old_table = {}
|
||||
self.new_table = {}
|
||||
if os.path.isdir("/sys_host/"):
|
||||
self.old_table = dict()
|
||||
self.new_table = dict(ctl_table)
|
||||
for name in ctl_table.keys():
|
||||
self.old_table[name] = read_sysctl(name)
|
||||
|
||||
def write_table(self, table) -> bool:
|
||||
for name, value in table.items():
|
||||
if read_sysctl(name) != value:
|
||||
write_sysctl(name, value)
|
||||
|
||||
def set(self):
|
||||
self.write_table(self.new_table)
|
||||
|
||||
def reset(self):
|
||||
self.write_table(self.old_table)
|
||||
|
||||
def read_sysctl(name:str):
|
||||
with open(f"/sys_host/{name}", "rt") as f:
|
||||
return "1" in f.read()
|
||||
|
||||
def write_sysctl(name:str, value:bool):
|
||||
with open(f"/sys_host/{name}", "wt") as f:
|
||||
f.write("1" if value else "0")
|
||||
|
||||
def list_files(mypath):
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
return [f for f in listdir(mypath) if isfile(join(mypath, f))]
|
||||
|
||||
def ip_parse(ip:str):
|
||||
return str(ip_interface(ip).network)
|
||||
|
||||
def is_ip_parse(ip:str):
|
||||
try:
|
||||
ip_parse(ip)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def addr_parse(ip:str):
|
||||
return str(ip_address(ip))
|
||||
|
||||
def ip_family(ip:str):
|
||||
return "ip6" if ip_interface(ip).version == 6 else "ip"
|
||||
|
||||
def get_interfaces():
|
||||
def _get_interfaces():
|
||||
for int_name, interfs in psutil.net_if_addrs().items():
|
||||
for interf in interfs:
|
||||
if interf.family in [socket.AF_INET, socket.AF_INET6]:
|
||||
yield {"name": int_name, "addr":interf.address}
|
||||
return list(_get_interfaces())
|
||||
|
||||
def nftables_int_to_json(ip_int):
|
||||
ip_int = ip_parse(ip_int)
|
||||
ip_addr = str(ip_int).split("/")[0]
|
||||
ip_addr_cidr = int(str(ip_int).split("/")[1])
|
||||
return {"prefix": {"addr": ip_addr, "len": ip_addr_cidr}}
|
||||
|
||||
def nftables_json_to_int(ip_json_int):
|
||||
if isinstance(ip_json_int,str):
|
||||
return str(ip_parse(ip_json_int))
|
||||
else:
|
||||
return f'{ip_json_int["prefix"]["addr"]}/{ip_json_int["prefix"]["len"]}'
|
||||
|
||||
class Singleton(object):
|
||||
__instance = None
|
||||
def __new__(class_, *args, **kwargs):
|
||||
if not isinstance(class_.__instance, class_):
|
||||
class_.__instance = object.__new__(class_, *args, **kwargs)
|
||||
return class_.__instance
|
||||
|
||||
class NFTableManager(Singleton):
|
||||
|
||||
table_name = "firegex"
|
||||
|
||||
def __init__(self, init_cmd, reset_cmd):
|
||||
self.__init_cmds = init_cmd
|
||||
self.__reset_cmds = reset_cmd
|
||||
self.nft = nftables.Nftables()
|
||||
|
||||
def raw_cmd(self, *cmds):
|
||||
return self.nft.json_cmd({"nftables": list(cmds)})
|
||||
|
||||
def cmd(self, *cmds):
|
||||
code, out, err = self.raw_cmd(*cmds)
|
||||
if code == 0:
|
||||
return out
|
||||
else:
|
||||
raise Exception(err)
|
||||
|
||||
def init(self):
|
||||
self.reset()
|
||||
self.raw_cmd({"add":{"table":{"name":self.table_name,"family":"inet"}}})
|
||||
self.cmd(*self.__init_cmds)
|
||||
|
||||
def reset(self):
|
||||
self.raw_cmd(*self.__reset_cmds)
|
||||
|
||||
def list_rules(self, tables = None, chains = None):
|
||||
for filter in [ele["rule"] for ele in self.raw_list() if "rule" in ele ]:
|
||||
if tables and filter["table"] not in tables:
|
||||
continue
|
||||
if chains and filter["chain"] not in chains:
|
||||
continue
|
||||
yield filter
|
||||
|
||||
def raw_list(self):
|
||||
return self.cmd({"list": {"ruleset": None}})["nftables"]
|
||||
|
||||
def _json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json"):
|
||||
res = obj.model_dump(mode=mode, exclude_unset=not unset)
|
||||
if convert_keys:
|
||||
for from_k, to_k in convert_keys.items():
|
||||
if from_k in res:
|
||||
res[to_k] = res.pop(from_k)
|
||||
if exclude:
|
||||
for ele in exclude:
|
||||
if ele in res:
|
||||
del res[ele]
|
||||
return res
|
||||
|
||||
def json_like(obj: BaseModel|List[BaseModel], unset=False, convert_keys:dict[str, str]=None, exclude:list[str]=None, mode:str="json") -> dict:
|
||||
if isinstance(obj, list):
|
||||
return [_json_like(ele, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode) for ele in obj]
|
||||
return _json_like(obj, unset=unset, convert_keys=convert_keys, exclude=exclude, mode=mode)
|
||||
|
||||
def register_event(sio_server: AsyncServer, event_name: str, model: BaseModel, response_model: BaseModel|None = None):
|
||||
def decorator(func):
|
||||
@sio_server.on(event_name) # Automatically registers the event
|
||||
@wraps(func)
|
||||
async def wrapper(sid, data):
|
||||
try:
|
||||
# Parse and validate incoming data
|
||||
parsed_data = model.model_validate(data)
|
||||
except ValidationError:
|
||||
return json_like(StatusMessageModel(status=f"Invalid {event_name} request"))
|
||||
|
||||
# Call the original function with the parsed data
|
||||
result = await func(sid, parsed_data)
|
||||
# If a response model is provided, validate the output
|
||||
if response_model:
|
||||
try:
|
||||
parsed_result = response_model.model_validate(result)
|
||||
except ValidationError:
|
||||
traceback.print_exc()
|
||||
return json_like(StatusMessageModel(status=f"SERVER ERROR: Invalid {event_name} response"))
|
||||
else:
|
||||
parsed_result = result
|
||||
# Emit the validated result
|
||||
if parsed_result:
|
||||
if isinstance(parsed_result, BaseModel):
|
||||
return json_like(parsed_result)
|
||||
return parsed_result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def nicenessify(priority:int, pid:int|None=None):
|
||||
try:
|
||||
pid = os.getpid() if pid is None else pid
|
||||
ps = psutil.Process(pid)
|
||||
if os.name == 'posix':
|
||||
ps.nice(priority)
|
||||
except Exception as e:
|
||||
print(f"Error setting priority: {e} {traceback.format_exc()}")
|
||||
pass
|
||||
|
||||
116
docs/TRAFFIC_VIEWER.md
Normal file
116
docs/TRAFFIC_VIEWER.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Traffic Viewer - JSON Event Format
|
||||
|
||||
The traffic viewer is now fully integrated and supports **TCP, HTTP, and UDP** protocols. To enable structured event display, the NFProxy C++ binary (`backend/binsrc/nfproxy.cpp`) should emit JSON lines to stdout with the following format:
|
||||
|
||||
## JSON Event Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"ts": 1701964234567,
|
||||
"direction": "in",
|
||||
"src_ip": "192.168.1.100",
|
||||
"src_port": 54321,
|
||||
"dst_ip": "10.0.0.5",
|
||||
"dst_port": 443,
|
||||
"proto": "tcp",
|
||||
"size": 1420,
|
||||
"verdict": "accept",
|
||||
"filter": "filter_sanitize",
|
||||
"sample_hex": "474554202f20485454502f312e310d0a486f73743a206578616d706c652e636f6d..."
|
||||
}
|
||||
```
|
||||
|
||||
## Fields
|
||||
|
||||
- `ts` (required): Unix timestamp in milliseconds
|
||||
- `direction`: `"in"` (client→server) or `"out"` (server→client)
|
||||
- `src_ip`, `dst_ip`: Source and destination IP addresses
|
||||
- `src_port`, `dst_port`: Source and destination ports
|
||||
- `proto`: Protocol name (e.g., `"tcp"`, `"udp"`)
|
||||
- `size`: Packet/payload size in bytes
|
||||
- `verdict` (required): `"accept"`, `"drop"`, `"reject"`, or `"edited"`
|
||||
- `filter`: Name of the Python filter that processed this packet
|
||||
- `sample_hex`: Hex-encoded sample of payload (first 64-128 bytes recommended)
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. **Backward Compatibility**: The parser in `firegex.py::_stream_handler` only processes lines starting with `{`. Non-JSON output (logs, ACK messages) continues to work as before.
|
||||
|
||||
2. **Performance**: Emit JSON only when needed. Consider an env flag:
|
||||
```cpp
|
||||
bool emit_traffic_json = getenv("FIREGEX_TRAFFIC_JSON") != nullptr;
|
||||
if (emit_traffic_json) {
|
||||
std::cout << json_event << std::endl;
|
||||
}
|
||||
```
|
||||
|
||||
3. **Sample Code** (C++ with nlohmann/json or similar):
|
||||
```cpp
|
||||
#include <nlohmann/json.hpp>
|
||||
using json = nlohmann::json;
|
||||
|
||||
void emit_traffic_event(const PacketInfo& pkt, const char* verdict, const char* filter_name) {
|
||||
json event = {
|
||||
{"ts", current_timestamp_ms()},
|
||||
{"direction", pkt.is_inbound ? "in" : "out"},
|
||||
{"src_ip", pkt.src_addr},
|
||||
{"src_port", pkt.src_port},
|
||||
{"dst_ip", pkt.dst_addr},
|
||||
{"dst_port", pkt.dst_port},
|
||||
{"proto", pkt.protocol},
|
||||
{"size", pkt.payload_len},
|
||||
{"verdict", verdict},
|
||||
{"filter", filter_name},
|
||||
{"sample_hex", hex_encode(pkt.payload, std::min(64, pkt.payload_len))}
|
||||
};
|
||||
std::cout << event.dump() << std::endl;
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Without Binary Changes
|
||||
|
||||
The viewer works immediately—it will display "No traffic events yet" until the binary is updated. You can manually test the Socket.IO flow by emitting mock events from Python:
|
||||
|
||||
```python
|
||||
# In backend shell or script
|
||||
import asyncio
|
||||
import json
|
||||
from utils import socketio
|
||||
|
||||
async def emit_test_event():
|
||||
event = {
|
||||
"ts": int(time.time() * 1000),
|
||||
"direction": "in",
|
||||
"src_ip": "192.168.1.50",
|
||||
"src_port": 12345,
|
||||
"dst_ip": "10.0.0.1",
|
||||
"dst_port": 80,
|
||||
"proto": "tcp",
|
||||
"size": 512,
|
||||
"verdict": "accept",
|
||||
"filter": "test_filter"
|
||||
}
|
||||
await socketio.emit("nfproxy-traffic-YOUR_SERVICE_ID", event, room="nfproxy-traffic-YOUR_SERVICE_ID")
|
||||
```
|
||||
|
||||
## Current Features
|
||||
|
||||
✅ **Backend**:
|
||||
- Ring buffer stores last 500 events per service
|
||||
- REST endpoint: `GET /api/nfproxy/services/{id}/traffic?limit=500`
|
||||
- REST endpoint: `POST /api/nfproxy/services/{id}/traffic/clear`
|
||||
- Socket.IO channels: `nfproxy-traffic-{service_id}` for live events, `nfproxy-traffic-history` on join
|
||||
|
||||
✅ **Frontend**:
|
||||
- Live table view at `/nfproxy/{service_id}/traffic`
|
||||
- Client-side text filter (searches IP, verdict, filter name, proto)
|
||||
- Click row to view full event details + hex payload
|
||||
- Auto-scroll, clear history button
|
||||
- Accessible via new button (double-arrow icon) in ServiceDetails page
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Update `backend/binsrc/nfproxy.cpp` to emit JSON events as shown above
|
||||
2. Rebuild the C++ binary
|
||||
3. Start a service and generate traffic—viewer will populate in real-time
|
||||
4. Optionally add more filters (by verdict, time range) or export to PCAP
|
||||
@@ -10,7 +10,7 @@ from firegex.nfproxy.internals.exceptions import (
|
||||
from firegex.nfproxy.internals.models import FullStreamAction, ExceptionAction
|
||||
from dataclasses import dataclass, field
|
||||
from collections import deque
|
||||
from zstd import ZSTD_uncompress
|
||||
import zstandard as zstd
|
||||
import gzip
|
||||
import io
|
||||
import zlib
|
||||
@@ -200,7 +200,7 @@ class InternalCallbackHandler:
|
||||
break
|
||||
elif enc == "zstd":
|
||||
try:
|
||||
decoding_body = ZSTD_uncompress(decoding_body)
|
||||
decoding_body = zstd.decompress(decoding_body)
|
||||
except Exception as e:
|
||||
print(f"Error decompressing zstd: {e}: skipping", flush=True)
|
||||
decode_success = False
|
||||
|
||||
@@ -2,7 +2,6 @@ typer
|
||||
pydantic>=2
|
||||
typing-extensions>=4.7.1
|
||||
pycryptodome
|
||||
zstd
|
||||
watchfiles
|
||||
fgex
|
||||
websockets
|
||||
|
||||
@@ -13,7 +13,10 @@ import { Firewall } from './pages/Firewall';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import NFProxy from './pages/NFProxy';
|
||||
import ServiceDetailsNFProxy from './pages/NFProxy/ServiceDetails';
|
||||
import { useAuth } from './js/store';
|
||||
import TrafficViewer from './pages/NFProxy/TrafficViewer';
|
||||
import TrafficViewerMain from './pages/TrafficViewer';
|
||||
import SetupPage from './pages/Setup';
|
||||
import { useAuthStore } from './js/store';
|
||||
|
||||
function App() {
|
||||
|
||||
@@ -23,7 +26,7 @@ function App() {
|
||||
const [error, setError] = useState<string|null>()
|
||||
const [loadinBtn, setLoadingBtn] = useState(false);
|
||||
const queryClient = useQueryClient()
|
||||
const { isAuthenticated, access_token } = useAuth()
|
||||
const { access_token } = useAuthStore()
|
||||
|
||||
useEffect(()=>{
|
||||
socketio.auth = { token: access_token || "" }
|
||||
@@ -43,7 +46,7 @@ function App() {
|
||||
socketio.off("connect_error")
|
||||
socketio.disconnect()
|
||||
}
|
||||
},[isAuthenticated])
|
||||
},[access_token])
|
||||
|
||||
const getStatus = () =>{
|
||||
getstatus().then( res =>{
|
||||
@@ -92,6 +95,7 @@ function App() {
|
||||
}
|
||||
}).catch( err => setError(err.toString()))
|
||||
setLoadingBtn(false)
|
||||
form.reset()
|
||||
}
|
||||
|
||||
|
||||
@@ -119,12 +123,14 @@ function App() {
|
||||
setLoadingBtn(true)
|
||||
await login(values).then(res => {
|
||||
if(!res){
|
||||
queryClient.invalidateQueries()
|
||||
setSystemStatus({...systemStatus, loggined:true})
|
||||
}else{
|
||||
setError("Login failed")
|
||||
}
|
||||
}).catch( err => setError(err.toString()))
|
||||
setLoadingBtn(false)
|
||||
form.reset()
|
||||
}
|
||||
|
||||
|
||||
@@ -169,9 +175,12 @@ const PageRouting = ({ getStatus }:{ getStatus:()=>void }) => {
|
||||
</Route>
|
||||
<Route path="nfproxy" element={<NFProxy><Outlet /></NFProxy>} >
|
||||
<Route path=":srv" element={<ServiceDetailsNFProxy />} />
|
||||
<Route path=":srv/traffic" element={<TrafficViewer />} />
|
||||
</Route>
|
||||
<Route path="traffic" element={<TrafficViewerMain />} />
|
||||
<Route path="firewall" element={<Firewall />} />
|
||||
<Route path="porthijack" element={<PortHijack />} />
|
||||
<Route path="setup" element={<SetupPage />} />
|
||||
<Route path="*" element={<HomeRedirector />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -1,115 +1,115 @@
|
||||
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useState } from 'react';
|
||||
import { RegexAddForm } from '../js/models';
|
||||
import { b64decode, b64encode, okNotify } from '../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex } from './NFRegex/utils';
|
||||
|
||||
type RegexAddInfo = {
|
||||
regex:string,
|
||||
mode:string,
|
||||
is_case_insensitive:boolean,
|
||||
deactive:boolean
|
||||
}
|
||||
|
||||
function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:string }) {
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
regex:"",
|
||||
mode:"C",
|
||||
is_case_insensitive:false,
|
||||
deactive:false
|
||||
},
|
||||
validate:{
|
||||
regex: (value) => value !== "" ? null : "Regex is required",
|
||||
mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode",
|
||||
}
|
||||
})
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = (values:RegexAddInfo) => {
|
||||
setSubmitLoading(true)
|
||||
|
||||
const request:RegexAddForm = {
|
||||
is_case_sensitive: !values.is_case_insensitive,
|
||||
service_id: service,
|
||||
mode: values.mode?values.mode:"B",
|
||||
regex: b64encode(values.regex),
|
||||
active: !values.deactive
|
||||
}
|
||||
setSubmitLoading(false)
|
||||
nfregex.regexesadd(request).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`)
|
||||
}else if (res.toLowerCase() === "invalid regex"){
|
||||
setSubmitLoading(false)
|
||||
form.setFieldError("regex", "Invalid Regex")
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Error: [ "+res+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
<TextInput
|
||||
label="Regex"
|
||||
placeholder="[A-Z0-9]{31}="
|
||||
{...form.getInputProps('regex')}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Case insensitive"
|
||||
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Deactivate"
|
||||
{...form.getInputProps('deactive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Select
|
||||
data={[
|
||||
{ value: 'C', label: 'Client -> Server' },
|
||||
{ value: 'S', label: 'Server -> Client' },
|
||||
{ value: 'B', label: 'Both (Client <-> Server)' },
|
||||
]}
|
||||
label="Choose the source of the packets to filter"
|
||||
variant="filled"
|
||||
{...form.getInputProps('mode')}
|
||||
/>
|
||||
<Group align="right" mt="md">
|
||||
<Button loading={submitLoading} type="submit">Add Filter</Button>
|
||||
</Group>
|
||||
|
||||
<Space h="md" />
|
||||
|
||||
{error?<>
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" /></>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddNewRegex;
|
||||
import { Button, Group, Space, TextInput, Notification, Switch, Modal, Select } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useState } from 'react';
|
||||
import { RegexAddForm } from '../js/models';
|
||||
import { b64decode, b64encode, okNotify } from '../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex } from './NFRegex/utils';
|
||||
|
||||
type RegexAddInfo = {
|
||||
regex:string,
|
||||
mode:string,
|
||||
is_case_insensitive:boolean,
|
||||
deactive:boolean
|
||||
}
|
||||
|
||||
function AddNewRegex({ opened, onClose, service }:{ opened:boolean, onClose:()=>void, service:string }) {
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
regex:"",
|
||||
mode:"C",
|
||||
is_case_insensitive:false,
|
||||
deactive:false
|
||||
},
|
||||
validate:{
|
||||
regex: (value) => value !== "" ? null : "Regex is required",
|
||||
mode: (value) => ['C', 'S', 'B'].includes(value) ? null : "Invalid mode",
|
||||
}
|
||||
})
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = (values:RegexAddInfo) => {
|
||||
setSubmitLoading(true)
|
||||
|
||||
const request:RegexAddForm = {
|
||||
is_case_sensitive: !values.is_case_insensitive,
|
||||
service_id: service,
|
||||
mode: values.mode?values.mode:"B",
|
||||
regex: b64encode(values.regex),
|
||||
active: !values.deactive
|
||||
}
|
||||
setSubmitLoading(false)
|
||||
nfregex.regexesadd(request).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Regex ${b64decode(request.regex)} has been added`, `Successfully added ${request.is_case_sensitive?"case sensitive":"case insensitive"} regex to ${request.service_id} service`)
|
||||
}else if (res.toLowerCase() === "invalid regex"){
|
||||
setSubmitLoading(false)
|
||||
form.setFieldError("regex", "Invalid Regex")
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Error: [ "+res+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title="Add a new regex filter" opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
<TextInput
|
||||
label="Regex"
|
||||
placeholder="[A-Z0-9]{31}="
|
||||
{...form.getInputProps('regex')}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Case insensitive"
|
||||
{...form.getInputProps('is_case_insensitive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Switch
|
||||
label="Deactivate"
|
||||
{...form.getInputProps('deactive', { type: 'checkbox' })}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Select
|
||||
data={[
|
||||
{ value: 'C', label: 'Client -> Server' },
|
||||
{ value: 'S', label: 'Server -> Client' },
|
||||
{ value: 'B', label: 'Both (Client <-> Server)' },
|
||||
]}
|
||||
label="Choose the source of the packets to filter"
|
||||
variant="filled"
|
||||
{...form.getInputProps('mode')}
|
||||
/>
|
||||
<Group align="right" mt="md">
|
||||
<Button loading={submitLoading} type="submit">Add Filter</Button>
|
||||
</Group>
|
||||
|
||||
<Space h="md" />
|
||||
|
||||
{error?<>
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" /></>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddNewRegex;
|
||||
|
||||
@@ -1,82 +1,82 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core';
|
||||
import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils';
|
||||
import { AiFillHome } from "react-icons/ai"
|
||||
import { useNavigate } from 'react-router';
|
||||
import { FaLock } from 'react-icons/fa';
|
||||
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
|
||||
import { ImExit } from 'react-icons/im';
|
||||
import ResetPasswordModal from './ResetPasswordModal';
|
||||
import ResetModal from './ResetModal';
|
||||
import { MenuDropDownWithButton } from '../MainLayout';
|
||||
import { useNavbarStore } from '../../js/store';
|
||||
|
||||
|
||||
function HeaderPage(props: any) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const { navOpened, toggleNav } = useNavbarStore()
|
||||
|
||||
const logout_action = () => {
|
||||
logout().then(r => {
|
||||
window.location.reload()
|
||||
}).catch(r => {
|
||||
errorNotify("Logout failed!",`Error: ${r}`)
|
||||
})
|
||||
}
|
||||
|
||||
const go_to_home = () => {
|
||||
navigator(`/${getMainPath()}`)
|
||||
}
|
||||
|
||||
const [changePasswordModal, setChangePasswordModal] = useState(false);
|
||||
const [resetFiregexModal, setResetFiregexModal] = useState(false);
|
||||
return <AppShell.Header className="firegex__header__header" {...props}>
|
||||
<Burger
|
||||
hiddenFrom='md'
|
||||
ml="lg"
|
||||
opened={navOpened}
|
||||
className="firegex__header__navbtn"
|
||||
onClick={toggleNav}
|
||||
size="sm"
|
||||
/>
|
||||
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center"}} ml={5}>
|
||||
<Box className="firegex__header__divlogo">
|
||||
<Tooltip zIndex={0} label="Home" openDelay={1000} color="dark" position="right" >
|
||||
<Image src="/header-logo.png" alt="Firegex logo" w={50} onClick={()=>navigator("/")}/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
|
||||
<Title order={2} >[Fi]*regex</Title>
|
||||
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a></p>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="flex-spacer" />
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Label>Firewall Access</Menu.Label>
|
||||
<Menu.Item leftSection={<FaLock size={14} />} onClick={() => setChangePasswordModal(true)}>Change Password</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label>Actions</Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md" />
|
||||
<Tooltip label="Home" position='bottom' color="teal">
|
||||
<ActionIcon color="teal" style={{marginRight:"10px"}}
|
||||
size="xl" radius="md" variant="filled"
|
||||
onClick={go_to_home}>
|
||||
<AiFillHome size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Logout" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled">
|
||||
<ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
|
||||
</Tooltip>
|
||||
<ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} />
|
||||
<ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} />
|
||||
<Space w="xl" />
|
||||
</AppShell.Header>
|
||||
}
|
||||
|
||||
export default HeaderPage;
|
||||
import React, { useState } from 'react';
|
||||
import { ActionIcon, Divider, Image, Menu, Tooltip, Burger, Space, AppShell, Box, Title } from '@mantine/core';
|
||||
import { errorNotify, getMainPath, isLargeScreen, logout } from '../../js/utils';
|
||||
import { AiFillHome } from "react-icons/ai"
|
||||
import { useNavigate } from 'react-router';
|
||||
import { FaLock } from 'react-icons/fa';
|
||||
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
|
||||
import { ImExit } from 'react-icons/im';
|
||||
import ResetPasswordModal from './ResetPasswordModal';
|
||||
import ResetModal from './ResetModal';
|
||||
import { MenuDropDownWithButton } from '../MainLayout';
|
||||
import { useNavbarStore } from '../../js/store';
|
||||
|
||||
|
||||
function HeaderPage(props: any) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const { navOpened, toggleNav } = useNavbarStore()
|
||||
|
||||
const logout_action = () => {
|
||||
logout().then(r => {
|
||||
window.location.reload()
|
||||
}).catch(r => {
|
||||
errorNotify("Logout failed!",`Error: ${r}`)
|
||||
})
|
||||
}
|
||||
|
||||
const go_to_home = () => {
|
||||
navigator(`/${getMainPath()}`)
|
||||
}
|
||||
|
||||
const [changePasswordModal, setChangePasswordModal] = useState(false);
|
||||
const [resetFiregexModal, setResetFiregexModal] = useState(false);
|
||||
return <AppShell.Header className="firegex__header__header" {...props}>
|
||||
<Burger
|
||||
hiddenFrom='md'
|
||||
ml="lg"
|
||||
opened={navOpened}
|
||||
className="firegex__header__navbtn"
|
||||
onClick={toggleNav}
|
||||
size="sm"
|
||||
/>
|
||||
<Box style={{ display: "flex", justifyContent: "center", alignItems: "center"}} ml={5}>
|
||||
<Box className="firegex__header__divlogo">
|
||||
<Tooltip zIndex={0} label="Home" openDelay={1000} color="dark" position="right" >
|
||||
<Image src="/header-logo.png" alt="Firegex logo" w={50} onClick={()=>navigator("/")}/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box display="flex" style={{ flexDirection: "column" }} visibleFrom='xs'>
|
||||
<Title order={2} >[Fi]*regex</Title>
|
||||
<p style={{margin: 0, fontSize: "70%"}}>By <a href="https://pwnzer0tt1.it">Pwnzer0tt1</a></p>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="flex-spacer" />
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Label>Firewall Access</Menu.Label>
|
||||
<Menu.Item leftSection={<FaLock size={14} />} onClick={() => setChangePasswordModal(true)}>Change Password</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label>Actions</Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<MdOutlineSettingsBackupRestore size={18} />} onClick={() => setResetFiregexModal(true)}>Reset Firegex</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md" />
|
||||
<Tooltip label="Home" position='bottom' color="teal">
|
||||
<ActionIcon color="teal" style={{marginRight:"10px"}}
|
||||
size="xl" radius="md" variant="filled"
|
||||
onClick={go_to_home}>
|
||||
<AiFillHome size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Logout" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={logout_action} size="xl" radius="md" variant="filled">
|
||||
<ImExit size={23} style={{marginTop:"3px", marginLeft:"2px"}}/></ActionIcon>
|
||||
</Tooltip>
|
||||
<ResetPasswordModal opened={changePasswordModal} onClose={() => setChangePasswordModal(false)} />
|
||||
<ResetModal opened={resetFiregexModal} onClose={() => setResetFiregexModal(false)} />
|
||||
<Space w="xl" />
|
||||
</AppShell.Header>
|
||||
}
|
||||
|
||||
export default HeaderPage;
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
import { useEffect } from 'react';
|
||||
import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core';
|
||||
import { AppShell } from '@mantine/core';
|
||||
import NavBar from './NavBar';
|
||||
import HeaderPage from './Header';
|
||||
import { getMainPath } from '../js/utils';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useNavbarStore } from '../js/store';
|
||||
import { HiMenu } from "react-icons/hi";
|
||||
|
||||
|
||||
function MainLayout({ children }:{ children:any }) {
|
||||
const { navOpened } = useNavbarStore()
|
||||
const location = useLocation()
|
||||
useEffect(()=>{
|
||||
if (location.pathname !== "/"){
|
||||
sessionStorage.setItem('home_section', getMainPath())
|
||||
}
|
||||
},[location.pathname])
|
||||
return <AppShell
|
||||
header={{ height: 70 }}
|
||||
navbar={{ width: 300 , breakpoint: "md", collapsed: { mobile: !navOpened } }}
|
||||
p="md"
|
||||
>
|
||||
<HeaderPage />
|
||||
<NavBar />
|
||||
<AppShell.Main>
|
||||
<Container size="lg">
|
||||
{children}
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
<Space h="lg" />
|
||||
|
||||
</AppShell>
|
||||
|
||||
}
|
||||
|
||||
export default MainLayout;
|
||||
|
||||
export const MenuDropDownWithButton = ({children}:{children:any}) => <Menu withArrow>
|
||||
<Menu.Target>
|
||||
<Tooltip label="More options" color="gray">
|
||||
<ActionIcon variant='transparent'>
|
||||
<HiMenu size={24} color='#FFF'/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{children}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
import { useEffect } from 'react';
|
||||
import { ActionIcon, Container, Menu, Space, Tooltip } from '@mantine/core';
|
||||
import { AppShell } from '@mantine/core';
|
||||
import NavBar from './NavBar';
|
||||
import HeaderPage from './Header';
|
||||
import { getMainPath } from '../js/utils';
|
||||
import { useLocation } from 'react-router';
|
||||
import { useNavbarStore } from '../js/store';
|
||||
import { HiMenu } from "react-icons/hi";
|
||||
|
||||
|
||||
function MainLayout({ children }:{ children:any }) {
|
||||
const { navOpened } = useNavbarStore()
|
||||
const location = useLocation()
|
||||
useEffect(()=>{
|
||||
if (location.pathname !== "/"){
|
||||
sessionStorage.setItem('home_section', getMainPath())
|
||||
}
|
||||
},[location.pathname])
|
||||
return <AppShell
|
||||
header={{ height: 70 }}
|
||||
navbar={{ width: 300 , breakpoint: "md", collapsed: { mobile: !navOpened } }}
|
||||
p="md"
|
||||
>
|
||||
<HeaderPage />
|
||||
<NavBar />
|
||||
<AppShell.Main>
|
||||
<Container size="lg">
|
||||
{children}
|
||||
</Container>
|
||||
</AppShell.Main>
|
||||
<Space h="lg" />
|
||||
|
||||
</AppShell>
|
||||
|
||||
}
|
||||
|
||||
export default MainLayout;
|
||||
|
||||
export const MenuDropDownWithButton = ({children}:{children:any}) => <Menu withArrow>
|
||||
<Menu.Target>
|
||||
<Tooltip label="More options" color="gray">
|
||||
<ActionIcon variant='transparent'>
|
||||
<HiMenu size={24} color='#FFF'/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{children}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@@ -1,139 +1,140 @@
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfproxy, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","http"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfproxy.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
{edit?null:<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>}
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfproxy, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","http","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfproxy.settings(edit.service_id, { port, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfproxy.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfproxy.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
{edit?null:<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>}
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
|
||||
@@ -72,7 +72,7 @@ export const HELP_NFPROXY_SIM = `➤ fgex nfproxy -h
|
||||
│ * port INTEGER The port of the target to proxy [default: None] [required] │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ --proto [tcp|http] The protocol to proxy [default: tcp] │
|
||||
│ --proto [tcp|http|udp] The protocol to proxy [default: tcp] │
|
||||
│ --from-address TEXT The address of the local server [default: None] │
|
||||
│ --from-port INTEGER The port of the local server [default: 7474] │
|
||||
│ -6 Use IPv6 for the connection │
|
||||
|
||||
@@ -1,164 +1,164 @@
|
||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfproxy, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected } from "react-icons/tb";
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
import { FaPencilAlt } from "react-icons/fa";
|
||||
import { ExceptionWarning } from '../ExceptionWarning';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<ExceptionWarning service_id={service.service_id} />
|
||||
<Space w="sm"/>
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Badge, Box, Divider, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfproxy, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected } from "react-icons/tb";
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
import { FaPencilAlt } from "react-icons/fa";
|
||||
import { ExceptionWarning } from '../ExceptionWarning';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {service.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {service.n_filters}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<ExceptionWarning service_id={service.service_id} />
|
||||
<Space w="sm"/>
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the filters associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,175 +1,182 @@
|
||||
import { PyFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
service_id:string,
|
||||
name:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_filters:number,
|
||||
edited_packets:number,
|
||||
blocked_packets:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfproxy","services"]
|
||||
|
||||
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
|
||||
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters"],
|
||||
queryFn:() => nfproxy.servicepyfilters(service_id)
|
||||
})
|
||||
|
||||
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
|
||||
queryFn:() => nfproxy.getpyfilterscode(service_id)
|
||||
})
|
||||
|
||||
export const nfproxy = {
|
||||
services: async () => {
|
||||
return await getapi("nfproxy/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}`) as Service;
|
||||
},
|
||||
pyfilterenable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
pyfilterdisable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfproxy/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicepyfilters: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
getpyfilterscode: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
||||
},
|
||||
setpyfilterscode: async (service_id:string, code:string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
|
||||
|
||||
# From here we can import the DataTypes that we want to use:
|
||||
# The data type must be specified in the filter functions
|
||||
# And will also interally be used to decide when call some filters and how aggregate data
|
||||
from firegex.nfproxy.models import RawPacket
|
||||
|
||||
# global context in this execution is dedicated to a single TCP stream
|
||||
# - This code will be executed once at the TCP stream start
|
||||
# - The filter will be called for each packet in the stream
|
||||
# - You can store in global context some data you need, but exceeding with data stored could be dangerous
|
||||
# - At the end of the stream the global context will be destroyed
|
||||
|
||||
from firegex.nfproxy import pyfilter
|
||||
# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
|
||||
|
||||
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
|
||||
# - The filter must return one of the following values:
|
||||
# - ACCEPT: The packet will be accepted
|
||||
# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream)
|
||||
# - UNSTABLE_MANGLE: The packet will be mangled and accepted
|
||||
# - DROP: All the packets in this stream will be easly dropped
|
||||
|
||||
# If you want, you can use print to debug your filters, but this could slow down the filter
|
||||
|
||||
# Filter names must be unique and are specified by the name of the function wrapped by the decorator
|
||||
@pyfilter
|
||||
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
|
||||
def strange_filter(packet:RawPacket):
|
||||
# Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
|
||||
# Also is not garanteed that l4_data is the same of the packet data:
|
||||
# packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
|
||||
# Unorder packets in TCP are accepted by default, and python is not called in this case
|
||||
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
|
||||
if b"TEST_MANGLING" in packet.l4_data:
|
||||
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
|
||||
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
|
||||
return UNSTABLE_MANGLE
|
||||
# Drops the traffic
|
||||
if b"BAD DATA 1" in packet.data:
|
||||
return DROP
|
||||
# Rejects the traffic
|
||||
if b"BAD DATA 2" in packet.data:
|
||||
return REJECT
|
||||
# Accepts the traffic (default if None is returned)
|
||||
return ACCEPT
|
||||
|
||||
# Example with a higher level of abstraction
|
||||
@pyfilter
|
||||
def http_filter(http:HTTPRequest):
|
||||
if http.method == "GET" and "test" in http.url:
|
||||
return REJECT
|
||||
|
||||
# ADVANCED OPTIONS
|
||||
# You can specify some additional options on the streaming managment
|
||||
# pyproxy will automatically store all the packets (already ordered by the c++ binary):
|
||||
#
|
||||
# If the stream is too big, you can specify what actions to take:
|
||||
# This can be done defining some variables in the global context
|
||||
# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB)
|
||||
# NOTE: the stream size is calculated and managed indipendently by the data type handling system
|
||||
# Only types required by at least 1 filter will be stored.
|
||||
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
|
||||
# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
|
||||
# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
|
||||
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
|
||||
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic
|
||||
|
||||
from firege.nfproxy import FullStreamAction
|
||||
|
||||
# Example of a global context
|
||||
FGEX_STREAM_MAX_SIZE = 4096
|
||||
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT
|
||||
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic
|
||||
`
|
||||
import { PyFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
service_id:string,
|
||||
name:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_filters:number,
|
||||
edited_packets:number,
|
||||
blocked_packets:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfproxy","services"]
|
||||
|
||||
export const nfproxyServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfproxy.services})
|
||||
export const nfproxyServicePyfiltersQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters"],
|
||||
queryFn:() => nfproxy.servicepyfilters(service_id)
|
||||
})
|
||||
|
||||
export const nfproxyServiceFilterCodeQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"pyfilters","code"],
|
||||
queryFn:() => nfproxy.getpyfilterscode(service_id)
|
||||
})
|
||||
|
||||
export const nfproxy = {
|
||||
services: async () => {
|
||||
return await getapi("nfproxy/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}`) as Service;
|
||||
},
|
||||
pyfilterenable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
pyfilterdisable: async (service_id:string, filter_name:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/pyfilters/${filter_name}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfproxy/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfproxy/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicepyfilters: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/pyfilters`) as PyFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
getpyfilterscode: async (service_id:string) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/code`) as string;
|
||||
},
|
||||
setpyfilterscode: async (service_id:string, code:string) => {
|
||||
const { status } = await putapi(`nfproxy/services/${service_id}/code`,{ code }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
gettraffic: async (service_id:string, limit:number = 500) => {
|
||||
return await getapi(`nfproxy/services/${service_id}/traffic?limit=${limit}`) as { events: any[], count: number };
|
||||
},
|
||||
cleartraffic: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfproxy/services/${service_id}/traffic/clear`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const EXAMPLE_PYFILTER = `# This in an example of a filter file with http protocol
|
||||
|
||||
# From here we can import the DataTypes that we want to use:
|
||||
# The data type must be specified in the filter functions
|
||||
# And will also interally be used to decide when call some filters and how aggregate data
|
||||
from firegex.nfproxy.models import RawPacket
|
||||
|
||||
# global context in this execution is dedicated to a single TCP stream
|
||||
# - This code will be executed once at the TCP stream start
|
||||
# - The filter will be called for each packet in the stream
|
||||
# - You can store in global context some data you need, but exceeding with data stored could be dangerous
|
||||
# - At the end of the stream the global context will be destroyed
|
||||
|
||||
from firegex.nfproxy import pyfilter
|
||||
# pyfilter is a decorator, this will make the function become an effective filter and must have parameters with a specified type
|
||||
|
||||
from firegex.nfproxy import REJECT, ACCEPT, UNSTABLE_MANGLE, DROP
|
||||
# - The filter must return one of the following values:
|
||||
# - ACCEPT: The packet will be accepted
|
||||
# - REJECT: The packet will be rejected (will be activated a mechanism to send a RST packet and drop all data in the stream)
|
||||
# - UNSTABLE_MANGLE: The packet will be mangled and accepted
|
||||
# - DROP: All the packets in this stream will be easly dropped
|
||||
|
||||
# If you want, you can use print to debug your filters, but this could slow down the filter
|
||||
|
||||
# Filter names must be unique and are specified by the name of the function wrapped by the decorator
|
||||
@pyfilter
|
||||
# This function will handle only a RawPacket object, this is the lowest level of the packet abstraction
|
||||
def strange_filter(packet:RawPacket):
|
||||
# Mangling packets can be dangerous, due to instability of the internal TCP state mangling done by the filter below
|
||||
# Also is not garanteed that l4_data is the same of the packet data:
|
||||
# packet data is the assembled TCP stream, l4_data is the TCP payload of the packet in the nfqueue
|
||||
# Unorder packets in TCP are accepted by default, and python is not called in this case
|
||||
# For this reason mangling will be only available RawPacket: higher level data abstraction will be read-only
|
||||
if b"TEST_MANGLING" in packet.l4_data:
|
||||
# It's possible to change teh raw_packet and l4_data values for mangling the packet, data is immutable instead
|
||||
packet.l4_data = packet.l4_data.replace(b"TEST", b"UNSTABLE")
|
||||
return UNSTABLE_MANGLE
|
||||
# Drops the traffic
|
||||
if b"BAD DATA 1" in packet.data:
|
||||
return DROP
|
||||
# Rejects the traffic
|
||||
if b"BAD DATA 2" in packet.data:
|
||||
return REJECT
|
||||
# Accepts the traffic (default if None is returned)
|
||||
return ACCEPT
|
||||
|
||||
# Example with a higher level of abstraction
|
||||
@pyfilter
|
||||
def http_filter(http:HTTPRequest):
|
||||
if http.method == "GET" and "test" in http.url:
|
||||
return REJECT
|
||||
|
||||
# ADVANCED OPTIONS
|
||||
# You can specify some additional options on the streaming managment
|
||||
# pyproxy will automatically store all the packets (already ordered by the c++ binary):
|
||||
#
|
||||
# If the stream is too big, you can specify what actions to take:
|
||||
# This can be done defining some variables in the global context
|
||||
# - FGEX_STREAM_MAX_SIZE: The maximum size of the stream in bytes (default 1MB)
|
||||
# NOTE: the stream size is calculated and managed indipendently by the data type handling system
|
||||
# Only types required by at least 1 filter will be stored.
|
||||
# - FGEX_FULL_STREAM_ACTION: The action to do when the stream is full
|
||||
# - FullStreamAction.FLUSH: Flush the stream and continue to acquire new packets (default)
|
||||
# - FullStreamAction.DROP: Drop the next stream packets - like a DROP action by filter
|
||||
# - FullStreamAction.REJECT: Reject the stream and close the connection - like a REJECT action by filter
|
||||
# - FullStreamAction.ACCEPT: Stops to call pyfilters and accept the traffic
|
||||
|
||||
from firege.nfproxy import FullStreamAction
|
||||
|
||||
# Example of a global context
|
||||
FGEX_STREAM_MAX_SIZE = 4096
|
||||
FGEX_FULL_STREAM_ACTION = FullStreamAction.REJECT
|
||||
# This could be an ideal configuration if we expect to normally have streams with a maximum size of 4KB of traffic
|
||||
`
|
||||
|
||||
@@ -1,139 +1,139 @@
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfregex.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
import { Button, Group, Space, TextInput, Notification, Modal, Switch, SegmentedControl, Box, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { okNotify, regex_ipv4, regex_ipv6 } from '../../js/utils';
|
||||
import { ImCross } from "react-icons/im"
|
||||
import { nfregex, Service } from './utils';
|
||||
import PortAndInterface from '../PortAndInterface';
|
||||
import { IoMdInformationCircleOutline } from "react-icons/io";
|
||||
import { ServiceAddForm as ServiceAddFormOriginal } from './utils';
|
||||
|
||||
type ServiceAddForm = ServiceAddFormOriginal & {autostart: boolean}
|
||||
|
||||
function AddEditService({ opened, onClose, edit }:{ opened:boolean, onClose:()=>void, edit?:Service }) {
|
||||
|
||||
const initialValues = {
|
||||
name: "",
|
||||
port:edit?.port??8080,
|
||||
ip_int:edit?.ip_int??"",
|
||||
proto:edit?.proto??"tcp",
|
||||
fail_open: edit?.fail_open??false,
|
||||
autostart: true
|
||||
}
|
||||
|
||||
const form = useForm({
|
||||
initialValues: initialValues,
|
||||
validate:{
|
||||
name: (value) => edit? null : value !== "" ? null : "Service name is required",
|
||||
port: (value) => (value>0 && value<65536) ? null : "Invalid port",
|
||||
proto: (value) => ["tcp","udp"].includes(value) ? null : "Invalid protocol",
|
||||
ip_int: (value) => (value.match(regex_ipv6) || value.match(regex_ipv4)) ? null : "Invalid IP address",
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (opened){
|
||||
form.setInitialValues(initialValues)
|
||||
form.reset()
|
||||
}
|
||||
}, [opened])
|
||||
|
||||
const close = () =>{
|
||||
onClose()
|
||||
form.reset()
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const [submitLoading, setSubmitLoading] = useState(false)
|
||||
const [error, setError] = useState<string|null>(null)
|
||||
|
||||
const submitRequest = ({ name, port, autostart, proto, ip_int, fail_open }:ServiceAddForm) =>{
|
||||
setSubmitLoading(true)
|
||||
if (edit){
|
||||
nfregex.settings(edit.service_id, { port, proto, ip_int, fail_open }).then( res => {
|
||||
if (!res){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
okNotify(`Service ${name} settings updated`, `Successfully updated settings for service ${name}`)
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}else{
|
||||
nfregex.servicesadd({ name, port, proto, ip_int, fail_open }).then( res => {
|
||||
if (res.status === "ok" && res.service_id){
|
||||
setSubmitLoading(false)
|
||||
close();
|
||||
if (autostart) nfregex.servicestart(res.service_id)
|
||||
okNotify(`Service ${name} has been added`, `Successfully added service with port ${port}`)
|
||||
}else{
|
||||
setSubmitLoading(false)
|
||||
setError("Invalid request! [ "+res.status+" ]")
|
||||
}
|
||||
}).catch( err => {
|
||||
setSubmitLoading(false)
|
||||
setError("Request Failed! [ "+err+" ]")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return <Modal size="xl" title={edit?`Editing ${edit.name} service`:"Add a new service"} opened={opened} onClose={close} closeOnClickOutside={false} centered>
|
||||
<form onSubmit={form.onSubmit(submitRequest)}>
|
||||
{!edit?<TextInput
|
||||
label="Service name"
|
||||
placeholder="Challenge 01"
|
||||
{...form.getInputProps('name')}
|
||||
/>:null}
|
||||
<Space h="md" />
|
||||
<PortAndInterface form={form} int_name="ip_int" port_name="port" label={"Public IP Interface and port (ipv4/ipv6 + CIDR allowed)"} />
|
||||
<Space h="md" />
|
||||
|
||||
<Box className='center-flex'>
|
||||
<Box>
|
||||
{!edit?<Switch
|
||||
label="Auto-Start Service"
|
||||
{...form.getInputProps('autostart', { type: 'checkbox' })}
|
||||
/>:null}
|
||||
<Space h="sm" />
|
||||
<Switch
|
||||
label={<Box className='center-flex'>
|
||||
Enable fail-open nfqueue
|
||||
<Space w="xs" />
|
||||
<Tooltip label={<>
|
||||
Firegex use internally nfqueue to handle packets<br />enabling this option will allow packets to pass through the firewall <br /> in case the filtering is too slow or too many traffic is coming<br />
|
||||
</>}>
|
||||
<IoMdInformationCircleOutline size={15} />
|
||||
</Tooltip>
|
||||
</Box>}
|
||||
{...form.getInputProps('fail_open', { type: 'checkbox' })}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-spacer"></Box>
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
]}
|
||||
{...form.getInputProps('proto')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Group justify='flex-end' mt="md" mb="sm">
|
||||
<Button loading={submitLoading} type="submit" disabled={edit?!form.isDirty():false}>{edit?"Edit Service":"Add Service"}</Button>
|
||||
</Group>
|
||||
|
||||
{error?<>
|
||||
<Space h="md" />
|
||||
<Notification icon={<ImCross size={14} />} color="red" onClose={()=>{setError(null)}}>
|
||||
Error: {error}
|
||||
</Notification><Space h="md" />
|
||||
</>:null}
|
||||
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
}
|
||||
|
||||
export default AddEditService;
|
||||
|
||||
@@ -1,158 +1,158 @@
|
||||
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Badge, Box, Divider, Grid, Menu, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, Service, serviceQueryKey } from '../utils';
|
||||
import { MdDoubleArrow, MdOutlineArrowForwardIos } from "react-icons/md"
|
||||
import YesNoModal from '../../YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from './RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaFilter } from "react-icons/fa";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../AddEditService';
|
||||
|
||||
export default function ServiceRow({ service, onClick }:{ service:Service, onClick?:()=>void }) {
|
||||
|
||||
let status_color = "gray";
|
||||
switch(service.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} stopped successfully!`,`The service on ${service.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(service.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${service.name} started successfully!`,`The service on ${service.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${service.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(service.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${service.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return <>
|
||||
<Box className='firegex__nfregex__rowbox'>
|
||||
<Box className="firegex__nfregex__row" style={{width:"100%", flexDirection: isMedium?"row":"column"}}>
|
||||
<Box>
|
||||
<Box className="center-flex" style={{ justifyContent: "flex-start" }}>
|
||||
<MdDoubleArrow size={30} style={{color: "white"}}/>
|
||||
<Title className="firegex__nfregex__name" ml="xs">
|
||||
{service.name}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box className="center-flex" style={{ gap: 8, marginTop: 15, justifyContent: "flex-start" }}>
|
||||
<Badge color={status_color} radius="md" size="lg" variant="filled">{service.status}</Badge>
|
||||
<Badge size="lg" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" style={{ fontSize: "110%" }}>
|
||||
:{service.port}
|
||||
</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space w="xl" />}
|
||||
</Box>
|
||||
|
||||
<Box className={isMedium?"center-flex":"center-flex-row"}>
|
||||
<Box className="center-flex-row">
|
||||
<Badge color={service.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled">{service.ip_int} on {service.proto}</Badge>
|
||||
<Space h="xs" />
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {service.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {service.n_regex}</Badge>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?<Space w="xl" />:<Space h="lg" />}
|
||||
<Box className="center-flex">
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={service.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(service.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{isMedium?<Space w="xl" />:<Space w="md" />}
|
||||
{onClick?<Box className='firegex__service_forward_btn'>
|
||||
<MdOutlineArrowForwardIos onClick={onClick} style={{cursor:"pointer"}} size={25} />
|
||||
</Box>:null}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${service.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={service}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={service}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
import { RegexFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { RegexAddForm } from "../../js/models"
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
name:string,
|
||||
service_id:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_packets:number,
|
||||
n_regex:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
proto?:string,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfregex","services"]
|
||||
|
||||
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
|
||||
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"regexes"],
|
||||
queryFn:() => nfregex.serviceregexes(service_id)
|
||||
})
|
||||
|
||||
export const nfregex = {
|
||||
services: async () => {
|
||||
return await getapi("nfregex/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}`) as Service;
|
||||
},
|
||||
regexdelete: async (regex_id:number) => {
|
||||
const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexenable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexdisable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfregex/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexesadd: async (data:RegexAddForm) => {
|
||||
const { status } = await postapi("nfregex/regexes",data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
serviceregexes: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
import { RegexFilter, ServerResponse } from "../../js/models"
|
||||
import { deleteapi, getapi, postapi, putapi } from "../../js/utils"
|
||||
import { RegexAddForm } from "../../js/models"
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
export type Service = {
|
||||
name:string,
|
||||
service_id:string,
|
||||
status:string,
|
||||
port:number,
|
||||
proto: string,
|
||||
ip_int: string,
|
||||
n_packets:number,
|
||||
n_regex:number,
|
||||
fail_open:boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddForm = {
|
||||
name:string,
|
||||
port:number,
|
||||
proto:string,
|
||||
ip_int:string,
|
||||
fail_open: boolean,
|
||||
}
|
||||
|
||||
export type ServiceSettings = {
|
||||
port?:number,
|
||||
proto?:string,
|
||||
ip_int?:string,
|
||||
fail_open?: boolean,
|
||||
}
|
||||
|
||||
export type ServiceAddResponse = {
|
||||
status: string,
|
||||
service_id?: string,
|
||||
}
|
||||
|
||||
export const serviceQueryKey = ["nfregex","services"]
|
||||
|
||||
export const nfregexServiceQuery = () => useQuery({queryKey:serviceQueryKey, queryFn:nfregex.services})
|
||||
export const nfregexServiceRegexesQuery = (service_id:string) => useQuery({
|
||||
queryKey:[...serviceQueryKey,service_id,"regexes"],
|
||||
queryFn:() => nfregex.serviceregexes(service_id)
|
||||
})
|
||||
|
||||
export const nfregex = {
|
||||
services: async () => {
|
||||
return await getapi("nfregex/services") as Service[];
|
||||
},
|
||||
serviceinfo: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}`) as Service;
|
||||
},
|
||||
regexdelete: async (regex_id:number) => {
|
||||
const { status } = await deleteapi(`nfregex/regexes/${regex_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexenable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/enable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexdisable: async (regex_id:number) => {
|
||||
const { status } = await postapi(`nfregex/regexes/${regex_id}/disable`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestart: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/start`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicerename: async (service_id:string, name: string) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/rename`,{ name }) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicestop: async (service_id:string) => {
|
||||
const { status } = await postapi(`nfregex/services/${service_id}/stop`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
servicesadd: async (data:ServiceAddForm) => {
|
||||
return await postapi("nfregex/services",data) as ServiceAddResponse;
|
||||
},
|
||||
servicedelete: async (service_id:string) => {
|
||||
const { status } = await deleteapi(`nfregex/services/${service_id}`) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
regexesadd: async (data:RegexAddForm) => {
|
||||
const { status } = await postapi("nfregex/regexes",data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
serviceregexes: async (service_id:string) => {
|
||||
return await getapi(`nfregex/services/${service_id}/regexes`) as RegexFilter[];
|
||||
},
|
||||
settings: async (service_id:string, data:ServiceSettings) => {
|
||||
const { status } = await putapi(`nfregex/services/${service_id}/settings`,data) as ServerResponse;
|
||||
return status === "ok"?undefined:status
|
||||
},
|
||||
}
|
||||
@@ -7,16 +7,17 @@ import { PiWallLight } from "react-icons/pi";
|
||||
import { useNavbarStore } from "../../js/store";
|
||||
import { getMainPath } from "../../js/utils";
|
||||
import { BsRegex } from "react-icons/bs";
|
||||
import { MdVisibility, MdSettings } from "react-icons/md";
|
||||
|
||||
function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick }:
|
||||
{ navigate?: string, closeNav: () => void, name:string, icon:any, color:MantineColor, disabled?:boolean, onClick?:CallableFunction }) {
|
||||
{ navigate?: string, closeNav: () => void, name: string, icon: any, color: MantineColor, disabled?: boolean, onClick?: CallableFunction }) {
|
||||
const navigator = useNavigate()
|
||||
return <UnstyledButton
|
||||
className={`firegex__navbar__unstyled_button${navigate==getMainPath()?" selected":""}${disabled?" disabled":""}`}
|
||||
onClick={()=>{
|
||||
if(navigate){navigator(`/${navigate}`);closeNav()}
|
||||
if (onClick) onClick()
|
||||
}} disabled={disabled}>
|
||||
className={`firegex__navbar__unstyled_button${navigate == getMainPath() ? " selected" : ""}${disabled ? " disabled" : ""}`}
|
||||
onClick={() => {
|
||||
if (navigate) { navigator(`/${navigate}`); closeNav() }
|
||||
if (onClick) onClick()
|
||||
}} disabled={disabled}>
|
||||
<Group>
|
||||
<ThemeIcon color={color} variant="light">
|
||||
{icon}
|
||||
@@ -24,7 +25,7 @@ function NavBarButton({ navigate, closeNav, name, icon, color, disabled, onClick
|
||||
<Text size="sm">{name}</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
}
|
||||
}
|
||||
|
||||
export default function NavBar() {
|
||||
const [toggle, setToggleState] = useState(false);
|
||||
@@ -35,17 +36,19 @@ export default function NavBar() {
|
||||
<Title order={4}>Options ⚙️</Title>
|
||||
</Box>
|
||||
<Divider my="xs" />
|
||||
<Box style={{flexGrow: 1}} component={ScrollArea} px="xs" mt="xs">
|
||||
<Box style={{ flexGrow: 1 }} component={ScrollArea} px="xs" mt="xs">
|
||||
<NavBarButton navigate="nfregex" closeNav={closeNav} name="Netfilter Regex" color="grape" icon={<BsRegex size={19} />} />
|
||||
<NavBarButton navigate="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} />} />
|
||||
<Box px="xs" mt="lg">
|
||||
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
||||
<NavBarButton navigate="traffic" closeNav={closeNav} name="Traffic Viewer" color="cyan" icon={<MdVisibility size={19} />} />
|
||||
<NavBarButton navigate="setup" closeNav={closeNav} name="Setup Import/Export" color="teal" icon={<MdSettings size={19} />} />
|
||||
{/* <Box px="xs" mt="lg">
|
||||
<Title order={5}>Experimental Features 🧪</Title>
|
||||
</Box>
|
||||
<Text></Text>
|
||||
<Divider my="xs" />
|
||||
<NavBarButton navigate="nfproxy" closeNav={closeNav} name="Netfilter Proxy" color="lime" icon={<TbPlugConnected size={19} />} />
|
||||
<Divider my="xs" /> */}
|
||||
</Box>
|
||||
|
||||
|
||||
</AppShell.Navbar>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -42,21 +42,6 @@ export const useAuthStore = create<AuthState>()(
|
||||
)
|
||||
);
|
||||
|
||||
// Hook personalizzati per un uso più facile nei componenti
|
||||
export const useAuth = () => {
|
||||
const { access_token, setAccessToken, clearAccessToken, getAccessToken } = useAuthStore();
|
||||
|
||||
const isAuthenticated = !!access_token;
|
||||
|
||||
return {
|
||||
access_token,
|
||||
isAuthenticated,
|
||||
setAccessToken,
|
||||
clearAccessToken,
|
||||
getAccessToken,
|
||||
};
|
||||
};
|
||||
|
||||
interface SessionState {
|
||||
home_section: string | null;
|
||||
setHomeSection: (section: string | null) => void;
|
||||
|
||||
@@ -1,227 +1,227 @@
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
import { ImCross } from "react-icons/im";
|
||||
import { TiTick } from "react-icons/ti"
|
||||
import { Navigate } from "react-router";
|
||||
import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models";
|
||||
import { Buffer } from "buffer"
|
||||
import { QueryClient, useQuery } from "@tanstack/react-query";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { io } from "socket.io-client";
|
||||
import { useAuthStore, useSessionStore } from "./store";
|
||||
|
||||
export const IS_DEV = import.meta.env.DEV
|
||||
|
||||
export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$";
|
||||
export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$";
|
||||
export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$"
|
||||
export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
|
||||
export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$"
|
||||
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
|
||||
export const DEV_IP_BACKEND = "127.0.0.1:4444"
|
||||
|
||||
export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes
|
||||
|
||||
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
|
||||
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
|
||||
|
||||
export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
|
||||
return value as E;
|
||||
}
|
||||
|
||||
export const socketio = import.meta.env.DEV?
|
||||
io("ws://"+DEV_IP_BACKEND, {
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
}):
|
||||
io({
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
})
|
||||
|
||||
export const queryClient = new QueryClient({ defaultOptions: { queries: {
|
||||
staleTime: Infinity
|
||||
} }})
|
||||
|
||||
export function getErrorMessage(e: any) {
|
||||
let error = "Unknown error";
|
||||
if(typeof e == "string") return e
|
||||
if (e.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
error = e.response.data.error;
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
error = e.message || e.error;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") {
|
||||
if (e.status){
|
||||
return e.status
|
||||
}
|
||||
if (e.detail){
|
||||
if (typeof e.detail == "string")
|
||||
return e.detail
|
||||
if (e.detail[0] && e.detail[0].msg)
|
||||
return e.detail[0].msg
|
||||
}
|
||||
if (e.error){
|
||||
return e.error
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
|
||||
export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
|
||||
return await new Promise((resolve, reject) => {
|
||||
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
|
||||
method: method,
|
||||
credentials: "same-origin",
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}),
|
||||
"Authorization" : "Bearer " + useAuthStore.getState().getAccessToken()
|
||||
},
|
||||
body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined
|
||||
}).then(res => {
|
||||
if(res.status === 401) window.location.reload()
|
||||
if(res.status === 406) resolve({status:"Wrong Password"})
|
||||
if(!res.ok){
|
||||
const errorDefault = res.statusText
|
||||
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
|
||||
}
|
||||
res.text().then(t => {
|
||||
try{
|
||||
resolve(JSON.parse(t))
|
||||
}catch(e){
|
||||
resolve(t)
|
||||
}
|
||||
}).catch( err => reject(err))
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function getapi(path:string):Promise<any>{
|
||||
return await genericapi("GET",path)
|
||||
}
|
||||
|
||||
export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise<any>{
|
||||
return await genericapi("POST",path,data,is_form)
|
||||
}
|
||||
|
||||
export async function deleteapi(path:string):Promise<any>{
|
||||
return await genericapi("DELETE",path)
|
||||
}
|
||||
|
||||
export async function putapi(path:string,data:any):Promise<any>{
|
||||
return await genericapi("PUT",path,data)
|
||||
}
|
||||
|
||||
export function getMainPath(){
|
||||
const paths = window.location.pathname.split("/")
|
||||
if (paths.length > 1) return paths[1]
|
||||
return ""
|
||||
}
|
||||
|
||||
export function HomeRedirector(){
|
||||
const section = useSessionStore.getState().getHomeSection();
|
||||
const path = section?`/${section}`:`/nfregex`
|
||||
return <Navigate to={path} replace/>
|
||||
}
|
||||
|
||||
export async function resetfiregex(delete_data:boolean = false){
|
||||
const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse;
|
||||
return (status === "ok"?undefined:status)
|
||||
}
|
||||
|
||||
export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces)
|
||||
|
||||
export async function getipinterfaces(){
|
||||
return await getapi("interfaces") as IpInterface[];
|
||||
}
|
||||
|
||||
export async function getstatus(){
|
||||
return await getapi(`status`) as ServerStatusResponse;
|
||||
}
|
||||
|
||||
export async function logout(){
|
||||
useAuthStore.getState().clearAccessToken();
|
||||
}
|
||||
|
||||
export async function setpassword(data:PasswordSend) {
|
||||
const { status, access_token } = await postapi("set-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function changepassword(data:ChangePassword) {
|
||||
const { status, access_token } = await postapi("change-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function login(data:PasswordSend) {
|
||||
const from = {username: "login", password: data.password};
|
||||
const { status, access_token } = await postapi("login",from,true) as LoginResponse;
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status;
|
||||
}
|
||||
|
||||
export function errorNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'red',
|
||||
icon: <ImCross />,
|
||||
});
|
||||
}
|
||||
|
||||
export function okNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'teal',
|
||||
icon: <TiTick />,
|
||||
});
|
||||
}
|
||||
|
||||
export const makeid = (length:number) => {
|
||||
let result = '';
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const charactersLength = characters.length;
|
||||
let counter = 0;
|
||||
while (counter < length) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
counter += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function b64encode(data:number[]|string){
|
||||
return Buffer.from(data).toString('base64')
|
||||
}
|
||||
|
||||
export function b64decode(regexB64:string){
|
||||
return Buffer.from(regexB64, "base64").toString()
|
||||
}
|
||||
|
||||
export function isMediumScreen(){
|
||||
return useMediaQuery('(min-width: 600px)');
|
||||
}
|
||||
|
||||
export function isLargeScreen(){
|
||||
return useMediaQuery('(min-width: 992px)');
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
import { ImCross } from "react-icons/im";
|
||||
import { TiTick } from "react-icons/ti"
|
||||
import { Navigate } from "react-router";
|
||||
import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerResponse, ServerResponseToken, ServerStatusResponse } from "./models";
|
||||
import { Buffer } from "buffer"
|
||||
import { QueryClient, useQuery } from "@tanstack/react-query";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { io } from "socket.io-client";
|
||||
import { useAuthStore, useSessionStore } from "./store";
|
||||
|
||||
export const IS_DEV = import.meta.env.DEV
|
||||
|
||||
export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$";
|
||||
export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$";
|
||||
export const regex_ipv4 = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))?$"
|
||||
export const regex_ipv4_no_cidr = "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
|
||||
export const regex_port = "^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?$"
|
||||
export const regex_range_port = "^(([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(-([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])?)?)?$"
|
||||
export const DEV_IP_BACKEND = "127.0.0.1:4444"
|
||||
|
||||
export const WARNING_NFPROXY_TIME_LIMIT = 1000*60*10 // 10 minutes
|
||||
|
||||
export type EnumToPrimitiveUnion<T> = `${T & string}` | ParseNumber<`${T & number}`>;
|
||||
type ParseNumber<T> = T extends `${infer U extends number}` ? U : never;
|
||||
|
||||
export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
|
||||
return value as E;
|
||||
}
|
||||
|
||||
export const socketio = import.meta.env.DEV?
|
||||
io("ws://"+DEV_IP_BACKEND, {
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
}):
|
||||
io({
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
token: useAuthStore.getState().getAccessToken()
|
||||
}
|
||||
})
|
||||
|
||||
export const queryClient = new QueryClient({ defaultOptions: { queries: {
|
||||
staleTime: Infinity
|
||||
} }})
|
||||
|
||||
export function getErrorMessage(e: any) {
|
||||
let error = "Unknown error";
|
||||
if(typeof e == "string") return e
|
||||
if (e.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
error = e.response.data.error;
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
error = e.message || e.error;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown error") {
|
||||
if (e.status){
|
||||
return e.status
|
||||
}
|
||||
if (e.detail){
|
||||
if (typeof e.detail == "string")
|
||||
return e.detail
|
||||
if (e.detail[0] && e.detail[0].msg)
|
||||
return e.detail[0].msg
|
||||
}
|
||||
if (e.error){
|
||||
return e.error
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
|
||||
export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
|
||||
return await new Promise((resolve, reject) => {
|
||||
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
|
||||
method: method,
|
||||
credentials: "same-origin",
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
...(data?{'Content-Type': is_form ? 'application/x-www-form-urlencoded' : 'application/json'}:{}),
|
||||
"Authorization" : "Bearer " + useAuthStore.getState().getAccessToken()
|
||||
},
|
||||
body: data? (is_form ? (new URLSearchParams(data)).toString() : JSON.stringify(data)) : undefined
|
||||
}).then(res => {
|
||||
if(res.status === 401) window.location.reload()
|
||||
if(res.status === 406) resolve({status:"Wrong Password"})
|
||||
if(!res.ok){
|
||||
const errorDefault = res.statusText
|
||||
return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault))
|
||||
}
|
||||
res.text().then(t => {
|
||||
try{
|
||||
resolve(JSON.parse(t))
|
||||
}catch(e){
|
||||
resolve(t)
|
||||
}
|
||||
}).catch( err => reject(err))
|
||||
}).catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function getapi(path:string):Promise<any>{
|
||||
return await genericapi("GET",path)
|
||||
}
|
||||
|
||||
export async function postapi(path:string,data:any=undefined,is_form:boolean=false):Promise<any>{
|
||||
return await genericapi("POST",path,data,is_form)
|
||||
}
|
||||
|
||||
export async function deleteapi(path:string):Promise<any>{
|
||||
return await genericapi("DELETE",path)
|
||||
}
|
||||
|
||||
export async function putapi(path:string,data:any):Promise<any>{
|
||||
return await genericapi("PUT",path,data)
|
||||
}
|
||||
|
||||
export function getMainPath(){
|
||||
const paths = window.location.pathname.split("/")
|
||||
if (paths.length > 1) return paths[1]
|
||||
return ""
|
||||
}
|
||||
|
||||
export function HomeRedirector(){
|
||||
const section = useSessionStore.getState().getHomeSection();
|
||||
const path = section?`/${section}`:`/nfregex`
|
||||
return <Navigate to={path} replace/>
|
||||
}
|
||||
|
||||
export async function resetfiregex(delete_data:boolean = false){
|
||||
const { status } = await postapi("reset",{delete:delete_data}) as ServerResponse;
|
||||
return (status === "ok"?undefined:status)
|
||||
}
|
||||
|
||||
export const ipInterfacesQuery = () => useQuery(["ipinterfaces"], getipinterfaces)
|
||||
|
||||
export async function getipinterfaces(){
|
||||
return await getapi("interfaces") as IpInterface[];
|
||||
}
|
||||
|
||||
export async function getstatus(){
|
||||
return await getapi(`status`) as ServerStatusResponse;
|
||||
}
|
||||
|
||||
export async function logout(){
|
||||
useAuthStore.getState().clearAccessToken();
|
||||
}
|
||||
|
||||
export async function setpassword(data:PasswordSend) {
|
||||
const { status, access_token } = await postapi("set-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function changepassword(data:ChangePassword) {
|
||||
const { status, access_token } = await postapi("change-password",data) as ServerResponseToken;
|
||||
if (access_token)
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status === "ok"?undefined:status
|
||||
}
|
||||
|
||||
export async function login(data:PasswordSend) {
|
||||
const from = {username: "login", password: data.password};
|
||||
const { status, access_token } = await postapi("login",from,true) as LoginResponse;
|
||||
useAuthStore.getState().setAccessToken(access_token);
|
||||
return status;
|
||||
}
|
||||
|
||||
export function errorNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'red',
|
||||
icon: <ImCross />,
|
||||
});
|
||||
}
|
||||
|
||||
export function okNotify(title:string, description:string ){
|
||||
showNotification({
|
||||
autoClose: 2000,
|
||||
title: title,
|
||||
message: description,
|
||||
color: 'teal',
|
||||
icon: <TiTick />,
|
||||
});
|
||||
}
|
||||
|
||||
export const makeid = (length:number) => {
|
||||
let result = '';
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const charactersLength = characters.length;
|
||||
let counter = 0;
|
||||
while (counter < length) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
counter += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function b64encode(data:number[]|string){
|
||||
return Buffer.from(data).toString('base64')
|
||||
}
|
||||
|
||||
export function b64decode(regexB64:string){
|
||||
return Buffer.from(regexB64, "base64").toString()
|
||||
}
|
||||
|
||||
export function isMediumScreen(){
|
||||
return useMediaQuery('(min-width: 600px)');
|
||||
}
|
||||
|
||||
export function isLargeScreen(){
|
||||
return useMediaQuery('(min-width: 992px)');
|
||||
}
|
||||
@@ -1,237 +1,243 @@
|
||||
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import PyFilterView from '../../components/PyFilterView';
|
||||
import { TbPlugConnected } from 'react-icons/tb';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { FaPython } from "react-icons/fa";
|
||||
import { FiFileText } from "react-icons/fi";
|
||||
import { ModalLog } from '../../components/ModalLog';
|
||||
import { useListState } from '@mantine/hooks';
|
||||
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
export default function ServiceDetailsNFProxy() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const services = nfproxyServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
const [openLogModal, setOpenLogModal] = useState(false)
|
||||
const [logData, logDataSetters] = useListState<string>([]);
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
if (srv){
|
||||
if (openLogModal){
|
||||
logDataSetters.setState([])
|
||||
socketio.emit("nfproxy-outstream-join", { service: srv });
|
||||
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
|
||||
logDataSetters.append(data)
|
||||
});
|
||||
}else{
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
return () => {
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
}
|
||||
}, [openLogModal, srv])
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={filtersList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<ExceptionWarning service_id={srv} />
|
||||
<Space w="sm" />
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Show logs" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
|
||||
<FiFileText size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider my="xl" />
|
||||
|
||||
{filterCode.data?<>
|
||||
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
|
||||
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
|
||||
</>: null}
|
||||
|
||||
{(!filtersList.data || filtersList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
|
||||
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
|
||||
}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
<ModalLog
|
||||
opened={openLogModal}
|
||||
close={()=>setOpenLogModal(false)}
|
||||
title={`Logs for service ${serviceInfo.name}`}
|
||||
data={logData.join("")}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Box, Code, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaFilter, FaPencilAlt, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceFilterCodeQuery, nfproxyServicePyfiltersQuery, nfproxyServiceQuery, serviceQueryKey } from '../../components/NFProxy/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4, socketio } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFProxy/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import PyFilterView from '../../components/PyFilterView';
|
||||
import { TbPlugConnected } from 'react-icons/tb';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { FaPython } from "react-icons/fa";
|
||||
import { FiFileText } from "react-icons/fi";
|
||||
import { ModalLog } from '../../components/ModalLog';
|
||||
import { useListState } from '@mantine/hooks';
|
||||
import { ExceptionWarning } from '../../components/NFProxy/ExceptionWarning';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
export default function ServiceDetailsNFProxy() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const services = nfproxyServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const filtersList = nfproxyServicePyfiltersQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const filterCode = nfproxyServiceFilterCodeQuery(srv??"")
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
const [openLogModal, setOpenLogModal] = useState(false)
|
||||
const [logData, logDataSetters] = useListState<string>([]);
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
if (srv){
|
||||
if (openLogModal){
|
||||
logDataSetters.setState([])
|
||||
socketio.emit("nfproxy-outstream-join", { service: srv });
|
||||
socketio.on(`nfproxy-outstream-${srv}`, (data) => {
|
||||
logDataSetters.append(data)
|
||||
});
|
||||
}else{
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
return () => {
|
||||
socketio.emit("nfproxy-outstream-leave", { service: srv });
|
||||
socketio.off(`nfproxy-outstream-${srv}`);
|
||||
logDataSetters.setState([])
|
||||
}
|
||||
}
|
||||
}, [openLogModal, srv])
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || filtersList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfproxy.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfproxy.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfproxy.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={filtersList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<ExceptionWarning service_id={srv} />
|
||||
<Space w="sm" />
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Show logs" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan" size="lg" radius="md" onClick={()=>setOpenLogModal(true)} loading={buttonLoading} variant="filled">
|
||||
<FiFileText size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs"/>
|
||||
<Tooltip label="Traffic viewer" zIndex={0} color="grape">
|
||||
<ActionIcon color="grape" size="lg" radius="md" onClick={()=>navigate(`/nfproxy/${srv}/traffic`)} variant="filled">
|
||||
<MdDoubleArrow size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2}} /> {serviceInfo.edited_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.blocked_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><TbPlugConnected style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_filters}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider my="xl" />
|
||||
|
||||
{filterCode.data?<>
|
||||
<Title order={3} style={{textAlign:"center"}} className="center-flex"><FaPython style={{ marginBottom: -3 }} size={30} /><Space w="xs" />Filter code</Title>
|
||||
<CodeHighlight code={filterCode.data} language="python" mt="lg" />
|
||||
</>: null}
|
||||
|
||||
{(!filtersList.data || filtersList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No filters found! Create some proxy filters, install the firegex client:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Read the documentation for more information<Space w="sm" /><DocsButton doc='nfproxy'/></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Then create a new filter file with the following syntax and upload it here (using the button above)</Title>
|
||||
</>:<>{filtersList.data?.map( (filterInfo) => <PyFilterView filterInfo={filterInfo} key={filterInfo.name}/>)}</>
|
||||
}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
<ModalLog
|
||||
opened={openLogModal}
|
||||
close={()=>setOpenLogModal(false)}
|
||||
title={`Logs for service ${serviceInfo.name}`}
|
||||
data={logData.join("")}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
306
frontend/src/pages/NFProxy/TrafficViewer.tsx
Normal file
306
frontend/src/pages/NFProxy/TrafficViewer.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import { ActionIcon, Badge, Box, Code, Divider, Grid, Group, LoadingOverlay, Modal, ScrollArea, Select, Space, Table, Text, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||
import { errorNotify, isMediumScreen, socketio } from '../../js/utils';
|
||||
import { FaArrowLeft, FaFilter, FaTrash } from 'react-icons/fa';
|
||||
import { MdDoubleArrow } from "react-icons/md";
|
||||
import { useListState } from '@mantine/hooks';
|
||||
|
||||
type TrafficEvent = {
|
||||
ts: number;
|
||||
direction?: string;
|
||||
src_ip?: string;
|
||||
src_port?: number;
|
||||
dst_ip?: string;
|
||||
dst_port?: number;
|
||||
proto?: string;
|
||||
size?: number;
|
||||
verdict?: string;
|
||||
filter?: string;
|
||||
sample_hex?: string;
|
||||
};
|
||||
|
||||
export default function TrafficViewer() {
|
||||
const { srv } = useParams();
|
||||
const services = nfproxyServiceQuery();
|
||||
const serviceInfo = services.data?.find((s: any) => s.service_id === srv);
|
||||
const navigate = useNavigate();
|
||||
const isMedium = isMediumScreen();
|
||||
const [events, eventsHandlers] = useListState<TrafficEvent>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filterText, setFilterText] = useState('');
|
||||
const [filterDirection, setFilterDirection] = useState<string | null>(null);
|
||||
const [filterProto, setFilterProto] = useState<string | null>(null);
|
||||
const [filterVerdict, setFilterVerdict] = useState<string | null>(null);
|
||||
const [selectedEvent, setSelectedEvent] = useState<TrafficEvent | null>(null);
|
||||
const [modalOpened, setModalOpened] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!srv) return;
|
||||
|
||||
// Fetch historical events
|
||||
const fetchHistory = async () => {
|
||||
try {
|
||||
const response = await nfproxy.gettraffic(srv, 500);
|
||||
if (response.events) {
|
||||
eventsHandlers.setState(response.events);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch traffic history:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchHistory();
|
||||
|
||||
// Join Socket.IO room
|
||||
socketio.emit("nfproxy-traffic-join", { service: srv });
|
||||
|
||||
// Listen for historical events on initial join
|
||||
socketio.on("nfproxy-traffic-history", (data: { events: TrafficEvent[] }) => {
|
||||
if (data.events && data.events.length > 0) {
|
||||
eventsHandlers.setState(data.events);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for live events
|
||||
socketio.on(`nfproxy-traffic-${srv}`, (event: TrafficEvent) => {
|
||||
eventsHandlers.append(event);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socketio.emit("nfproxy-traffic-leave", { service: srv });
|
||||
socketio.off(`nfproxy-traffic-${srv}`);
|
||||
socketio.off("nfproxy-traffic-history");
|
||||
};
|
||||
}, [srv]);
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />;
|
||||
if (!srv || !serviceInfo) return <Navigate to="/" replace />;
|
||||
|
||||
const clearEvents = async () => {
|
||||
try {
|
||||
await nfproxy.cleartraffic(srv);
|
||||
eventsHandlers.setState([]);
|
||||
} catch (err) {
|
||||
errorNotify("Failed to clear traffic events", String(err));
|
||||
}
|
||||
};
|
||||
|
||||
const filteredEvents = events.filter((e: TrafficEvent) => {
|
||||
// Text filter
|
||||
if (filterText) {
|
||||
const search = filterText.toLowerCase();
|
||||
const matchesText = (
|
||||
e.src_ip?.toLowerCase().includes(search) ||
|
||||
e.dst_ip?.toLowerCase().includes(search) ||
|
||||
e.verdict?.toLowerCase().includes(search) ||
|
||||
e.filter?.toLowerCase().includes(search) ||
|
||||
e.proto?.toLowerCase().includes(search) ||
|
||||
e.src_port?.toString().includes(search) ||
|
||||
e.dst_port?.toString().includes(search)
|
||||
);
|
||||
if (!matchesText) return false;
|
||||
}
|
||||
|
||||
// Direction filter
|
||||
if (filterDirection && e.direction !== filterDirection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Protocol filter
|
||||
if (filterProto && e.proto !== filterProto) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verdict filter
|
||||
if (filterVerdict && e.verdict !== filterVerdict) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const formatTimestamp = (ts: number) => {
|
||||
const date = new Date(ts);
|
||||
return date.toLocaleTimeString() + '.' + date.getMilliseconds().toString().padStart(3, '0');
|
||||
};
|
||||
|
||||
const getVerdictColor = (verdict?: string) => {
|
||||
switch (verdict?.toLowerCase()) {
|
||||
case 'accept': return 'teal';
|
||||
case 'drop': return 'red';
|
||||
case 'reject': return 'orange';
|
||||
case 'edited': return 'yellow';
|
||||
default: return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const openDetails = (event: TrafficEvent) => {
|
||||
setSelectedEvent(event);
|
||||
setModalOpened(true);
|
||||
};
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<Box className={isMedium ? 'center-flex' : 'center-flex-row'} style={{ justifyContent: "space-between" }} px="md" mt="lg">
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />Traffic Viewer - {serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="cyan" radius="md" size="xl" variant="filled" mr="sm">
|
||||
{filteredEvents.length} events
|
||||
</Badge>
|
||||
<Tooltip label="Clear events" color="red">
|
||||
<ActionIcon color="red" size="lg" radius="md" onClick={clearEvents} variant="filled">
|
||||
<FaTrash size="18px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md" />
|
||||
<Tooltip label="Go back" color="cyan">
|
||||
<ActionIcon color="cyan" onClick={() => navigate(-1)} size="lg" radius="md" variant="filled">
|
||||
<FaArrowLeft size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider my="md" />
|
||||
|
||||
<Box px="md">
|
||||
<Grid mb="md">
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<TextInput
|
||||
placeholder="Search by IP, port, verdict, filter name, or protocol..."
|
||||
value={filterText}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
|
||||
leftSection={<FaFilter />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 2 }}>
|
||||
<Select
|
||||
placeholder="Direction"
|
||||
clearable
|
||||
value={filterDirection}
|
||||
onChange={setFilterDirection}
|
||||
data={[
|
||||
{ value: 'in', label: 'Incoming' },
|
||||
{ value: 'out', label: 'Outgoing' }
|
||||
]}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 2 }}>
|
||||
<Select
|
||||
placeholder="Protocol"
|
||||
clearable
|
||||
value={filterProto}
|
||||
onChange={setFilterProto}
|
||||
data={[
|
||||
{ value: 'tcp', label: 'TCP' },
|
||||
{ value: 'udp', label: 'UDP' },
|
||||
{ value: 'http', label: 'HTTP' }
|
||||
]}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, md: 2 }}>
|
||||
<Select
|
||||
placeholder="Verdict"
|
||||
clearable
|
||||
value={filterVerdict}
|
||||
onChange={setFilterVerdict}
|
||||
data={[
|
||||
{ value: 'accept', label: 'Accept' },
|
||||
{ value: 'drop', label: 'Drop' },
|
||||
{ value: 'reject', label: 'Reject' },
|
||||
{ value: 'edited', label: 'Edited' }
|
||||
]}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<ScrollArea style={{ height: 'calc(100vh - 280px)' }}>
|
||||
<Table striped highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Time</Table.Th>
|
||||
<Table.Th>Direction</Table.Th>
|
||||
<Table.Th>Source</Table.Th>
|
||||
<Table.Th>Destination</Table.Th>
|
||||
<Table.Th>Protocol</Table.Th>
|
||||
<Table.Th>Size</Table.Th>
|
||||
<Table.Th>Filter</Table.Th>
|
||||
<Table.Th>Verdict</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{filteredEvents.length === 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={8} style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
<Text c="dimmed">
|
||||
{filterText ? 'No events match your filter' : 'No traffic events yet. Waiting for traffic...'}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : (
|
||||
filteredEvents.slice(-500).reverse().map((event: TrafficEvent, idx: number) => (
|
||||
<Table.Tr key={idx} onClick={() => openDetails(event)} style={{ cursor: 'pointer' }}>
|
||||
<Table.Td><Code>{formatTimestamp(event.ts)}</Code></Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm" variant="dot" color={event.direction === 'in' ? 'blue' : 'grape'}>
|
||||
{event.direction || 'unknown'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>{event.src_ip || '-'}:{event.src_port || '-'}</Table.Td>
|
||||
<Table.Td>{event.dst_ip || '-'}:{event.dst_port || '-'}</Table.Td>
|
||||
<Table.Td><Badge size="sm" color="violet">{event.proto || 'unknown'}</Badge></Table.Td>
|
||||
<Table.Td>{event.size ? `${event.size} B` : '-'}</Table.Td>
|
||||
<Table.Td><Code>{event.filter || '-'}</Code></Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={getVerdictColor(event.verdict)} size="sm">
|
||||
{event.verdict || 'unknown'}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</Box>
|
||||
|
||||
{/* Payload details modal */}
|
||||
<Modal
|
||||
opened={modalOpened}
|
||||
onClose={() => setModalOpened(false)}
|
||||
title="Event Details"
|
||||
size="xl"
|
||||
>
|
||||
{selectedEvent && (
|
||||
<Box>
|
||||
<Grid>
|
||||
<Grid.Col span={6}><strong>Timestamp:</strong> {formatTimestamp(selectedEvent.ts)}</Grid.Col>
|
||||
<Grid.Col span={6}><strong>Direction:</strong> {selectedEvent.direction || 'unknown'}</Grid.Col>
|
||||
<Grid.Col span={6}><strong>Source:</strong> {selectedEvent.src_ip}:{selectedEvent.src_port}</Grid.Col>
|
||||
<Grid.Col span={6}><strong>Destination:</strong> {selectedEvent.dst_ip}:{selectedEvent.dst_port}</Grid.Col>
|
||||
<Grid.Col span={6}><strong>Protocol:</strong> {selectedEvent.proto || 'unknown'}</Grid.Col>
|
||||
<Grid.Col span={6}><strong>Size:</strong> {selectedEvent.size ? `${selectedEvent.size} B` : '-'}</Grid.Col>
|
||||
<Grid.Col span={6}><strong>Filter:</strong> {selectedEvent.filter || '-'}</Grid.Col>
|
||||
<Grid.Col span={6}><strong>Verdict:</strong> {selectedEvent.verdict || 'unknown'}</Grid.Col>
|
||||
</Grid>
|
||||
{selectedEvent.sample_hex && (
|
||||
<>
|
||||
<Divider my="md" label="Payload Sample (Hex)" />
|
||||
<ScrollArea style={{ maxHeight: '300px' }}>
|
||||
<Code block>{selectedEvent.sample_hex}</Code>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Modal>
|
||||
</>;
|
||||
}
|
||||
@@ -1,172 +1,172 @@
|
||||
import { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFProxy/ServiceRow';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected, TbReload } from 'react-icons/tb';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
|
||||
import { MdUploadFile } from "react-icons/md";
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useFileDialog } from '@mantine/hooks';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
export default function NFProxy({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfproxyServiceQuery()
|
||||
const fileDialog = useFileDialog({
|
||||
accept: ".py",
|
||||
multiple: false,
|
||||
resetOnOpen: true,
|
||||
onChange: (files) => {
|
||||
if (files?.length??0 > 0)
|
||||
setFile(files![0])
|
||||
}
|
||||
});
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
useEffect(() => {
|
||||
if (!srv) return
|
||||
const service = services.data?.find(s => s.service_id === srv)
|
||||
if (!service) return
|
||||
if (file){
|
||||
console.log("Uploading code")
|
||||
const notify_id = notifications.show(
|
||||
{
|
||||
title: "Uploading code",
|
||||
message: `Uploading code for service ${service.name}`,
|
||||
color: "blue",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: false,
|
||||
loading: true,
|
||||
}
|
||||
)
|
||||
file.text()
|
||||
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
|
||||
.then( res => {
|
||||
if (!res){
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code uploaded",
|
||||
message: `Successfully uploaded code for service ${service.name}`,
|
||||
color: "green",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}else{
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${res}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
}).catch( err => {
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${err}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}).finally(()=>{setFile(null)})
|
||||
}
|
||||
}, [file])
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
|
||||
<MdUploadFile size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
|
||||
<BsPlusLg size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
|
||||
<TbReload size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfproxy" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfproxy/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter proxy is a simulated proxy written using python with a c++ core</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Then you can create a new service and write custom filters for the service</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfproxy" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{!srv?
|
||||
<AddEditService opened={open} onClose={closeModal} />:null
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
import { ActionIcon, Badge, Box, Code, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFProxy/ServiceRow';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFProxy/AddEditService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbPlugConnected, TbReload } from 'react-icons/tb';
|
||||
import { EXAMPLE_PYFILTER, nfproxy, nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||
import { FaFilter, FaPencilAlt, FaServer } from 'react-icons/fa';
|
||||
import { MdUploadFile } from "react-icons/md";
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useFileDialog } from '@mantine/hooks';
|
||||
import { CodeHighlight } from '@mantine/code-highlight';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
export default function NFProxy({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfproxyServiceQuery()
|
||||
const fileDialog = useFileDialog({
|
||||
accept: ".py",
|
||||
multiple: false,
|
||||
resetOnOpen: true,
|
||||
onChange: (files) => {
|
||||
if (files?.length??0 > 0)
|
||||
setFile(files![0])
|
||||
}
|
||||
});
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
useEffect(() => {
|
||||
if (!srv) return
|
||||
const service = services.data?.find(s => s.service_id === srv)
|
||||
if (!service) return
|
||||
if (file){
|
||||
console.log("Uploading code")
|
||||
const notify_id = notifications.show(
|
||||
{
|
||||
title: "Uploading code",
|
||||
message: `Uploading code for service ${service.name}`,
|
||||
color: "blue",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: false,
|
||||
loading: true,
|
||||
}
|
||||
)
|
||||
file.text()
|
||||
.then( code => nfproxy.setpyfilterscode(service?.service_id??"",code.toString()))
|
||||
.then( res => {
|
||||
if (!res){
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code uploaded",
|
||||
message: `Successfully uploaded code for service ${service.name}`,
|
||||
color: "green",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}else{
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${res}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
}).catch( err => {
|
||||
notifications.update({
|
||||
id: notify_id,
|
||||
title: "Code upload failed",
|
||||
message: `Error: ${err}`,
|
||||
color: "red",
|
||||
icon: <MdUploadFile size={20} />,
|
||||
autoClose: 5000,
|
||||
loading: false,
|
||||
})
|
||||
}).finally(()=>{setFile(null)})
|
||||
}
|
||||
}, [file])
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFProxy Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='lime' ><TbPlugConnected size={20} /></ThemeIcon><Space w="xs" />Netfilter Proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.blocked_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="orange" radius="sm" size="md" variant="filled"><FaPencilAlt style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.edited_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><TbPlugConnected style={{ marginBottom: -2, marginRight: 4}} size={13} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_filters, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Upload a new filter code" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" size="lg" radius="md" variant="filled" onClick={fileDialog.open}>
|
||||
<MdUploadFile size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled">
|
||||
<BsPlusLg size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfproxy"])} size="lg" radius="md" variant="filled" loading={services.isFetching}>
|
||||
<TbReload size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfproxy" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfproxy/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter proxy is a simulated proxy written using python with a c++ core</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Filters are created using a simple python syntax, infact the first you need to do is to install the firegex lib:<Space w="xs" /><Code mb={-4} >pip install -U fgex</Code></Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Then you can create a new service and write custom filters for the service</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfproxy" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{!srv?
|
||||
<AddEditService opened={open} onClose={closeModal} />:null
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,194 +1,194 @@
|
||||
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import RegexView from '../../components/RegexView';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { VscRegex } from 'react-icons/vsc';
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
|
||||
export default function ServiceDetailsNFRegex() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
const services = nfregexServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const regexesList = nfregexServiceRegexesQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={regexesList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider my="xl" />
|
||||
{(!regexesList.data || regexesList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
|
||||
<Space h="xl" /> <Space h="xl" />
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Add a new regex" zIndex={0} color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</>:
|
||||
<Grid>
|
||||
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
import { ActionIcon, Box, Grid, LoadingOverlay, Space, Title, Tooltip } from '@mantine/core';
|
||||
import { Navigate, useNavigate, useParams } from 'react-router';
|
||||
import RegexView from '../../components/RegexView';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import { nfregexServiceQuery, nfregexServiceRegexesQuery, Service } from '../../components/NFRegex/utils';
|
||||
import { Badge, Divider, Menu } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaFilter, FaPlay, FaStop } from 'react-icons/fa';
|
||||
import { nfregex, serviceQueryKey } from '../../components/NFRegex/utils';
|
||||
import { MdDoubleArrow } from "react-icons/md"
|
||||
import YesNoModal from '../../components/YesNoModal';
|
||||
import { errorNotify, isMediumScreen, okNotify, regex_ipv4 } from '../../js/utils';
|
||||
import { BsTrashFill } from 'react-icons/bs';
|
||||
import { BiRename } from 'react-icons/bi'
|
||||
import RenameForm from '../../components/NFRegex/ServiceRow/RenameForm';
|
||||
import { MenuDropDownWithButton } from '../../components/MainLayout';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FaArrowLeft } from "react-icons/fa";
|
||||
import { VscRegex } from 'react-icons/vsc';
|
||||
import { IoSettingsSharp } from 'react-icons/io5';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
|
||||
export default function ServiceDetailsNFRegex() {
|
||||
|
||||
const {srv} = useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
const services = nfregexServiceQuery()
|
||||
const serviceInfo = services.data?.find(s => s.service_id == srv)
|
||||
const regexesList = nfregexServiceRegexesQuery(srv??"")
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const [renameModal, setRenameModal] = useState(false)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [buttonLoading, setButtonLoading] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
const navigate = useNavigate()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
if (services.isLoading) return <LoadingOverlay visible={true} />
|
||||
if (!srv || !serviceInfo || regexesList.isError) return <Navigate to="/" replace />
|
||||
|
||||
let status_color = "gray";
|
||||
switch(serviceInfo.status){
|
||||
case "stop": status_color = "red"; break;
|
||||
case "active": status_color = "teal"; break;
|
||||
}
|
||||
|
||||
const startService = async () => {
|
||||
setButtonLoading(true)
|
||||
await nfregex.servicestart(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} started successfully!`,`The service on ${serviceInfo.port} has been started!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the starting of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false)
|
||||
}
|
||||
|
||||
const deleteService = () => {
|
||||
nfregex.servicedelete(serviceInfo.service_id).then(res => {
|
||||
if (!res){
|
||||
okNotify("Service delete complete!",`The service ${serviceInfo.name} has been deleted!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${res}`)
|
||||
}).catch(err => {
|
||||
errorNotify("An error occurred while deleting a service",`Error: ${err}`)
|
||||
})
|
||||
}
|
||||
|
||||
const stopService = async () => {
|
||||
setButtonLoading(true)
|
||||
|
||||
await nfregex.servicestop(serviceInfo.service_id).then(res => {
|
||||
if(!res){
|
||||
okNotify(`Service ${serviceInfo.name} stopped successfully!`,`The service on ${serviceInfo.port} has been stopped!`)
|
||||
queryClient.invalidateQueries(serviceQueryKey)
|
||||
}else{
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${res}`)
|
||||
}
|
||||
}).catch(err => {
|
||||
errorNotify(`An error as occurred during the stopping of the service ${serviceInfo.port}`,`Error: ${err}`)
|
||||
})
|
||||
setButtonLoading(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<LoadingOverlay visible={regexesList.isLoading} />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box>
|
||||
<Title order={1}>
|
||||
<Box className="center-flex">
|
||||
<MdDoubleArrow /><Space w="sm" />{serviceInfo.name}
|
||||
</Box>
|
||||
</Title>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge color={status_color} radius="md" size="xl" variant="filled" mr="sm">
|
||||
{serviceInfo.status}
|
||||
</Badge>
|
||||
<Badge size="xl" gradient={{ from: 'indigo', to: 'cyan' }} variant="gradient" radius="md" mr="sm">
|
||||
:{serviceInfo.port}
|
||||
</Badge>
|
||||
|
||||
<MenuDropDownWithButton>
|
||||
<Menu.Item><b>Edit service</b></Menu.Item>
|
||||
<Menu.Item leftSection={<IoSettingsSharp size={18} />} onClick={()=>setEditModal(true)}>Service Settings</Menu.Item>
|
||||
<Menu.Item leftSection={<BiRename size={18} />} onClick={()=>setRenameModal(true)}>Change service name</Menu.Item>
|
||||
<Divider />
|
||||
<Menu.Label><b>Danger zone</b></Menu.Label>
|
||||
<Menu.Item color="red" leftSection={<BsTrashFill size={18} />} onClick={()=>setDeleteModal(true)}>Delete Service</Menu.Item>
|
||||
</MenuDropDownWithButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'} style={{ justifyContent: "space-between"}} px="md" mt="lg">
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Box className='center-flex'>
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2}} /> {serviceInfo.n_packets}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="violet" radius="sm" size="md" variant="filled"><VscRegex style={{ marginBottom: -2}} size={13} /> {serviceInfo.n_regex}</Badge>
|
||||
</Box>
|
||||
{isMedium?<Space w="xs" />:<Space h="xs" />}
|
||||
<Badge color={serviceInfo.ip_int.match(regex_ipv4)?"cyan":"pink"} radius="sm" size="md" variant="filled" mr="xs">{serviceInfo.ip_int} on {serviceInfo.proto}</Badge>
|
||||
</Box>
|
||||
{isMedium?null:<Space h="xl" />}
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Go back" zIndex={0} color="cyan">
|
||||
<ActionIcon color="cyan"
|
||||
onClick={() => navigate("/")} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-back-id">
|
||||
<FaArrowLeft size="25px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Stop service" zIndex={0} color="red">
|
||||
<ActionIcon color="red" loading={buttonLoading}
|
||||
onClick={stopService} size="xl" radius="md" variant="filled"
|
||||
disabled={serviceInfo.status === "stop"}
|
||||
aria-describedby="tooltip-stop-id">
|
||||
<FaStop size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="md"/>
|
||||
<Tooltip label="Start service" zIndex={0} color="teal">
|
||||
<ActionIcon color="teal" size="xl" radius="md" onClick={startService} loading={buttonLoading}
|
||||
variant="filled" disabled={!["stop","pause"].includes(serviceInfo.status)?true:false}>
|
||||
<FaPlay size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider my="xl" />
|
||||
{(!regexesList.data || regexesList.data.length == 0)?<>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>No regex found for this service! Add one by clicking the "+" buttons</Title>
|
||||
<Space h="xl" /> <Space h="xl" />
|
||||
<Box className='center-flex'>
|
||||
<Tooltip label="Add a new regex" zIndex={0} color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled"
|
||||
aria-describedby="tooltip-AddRegex-id"><BsPlusLg size="20px" /></ActionIcon>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</>:
|
||||
<Grid>
|
||||
{regexesList.data?.map( (regexInfo) => <Grid.Col key={regexInfo.id} span={{ lg:6, xs: 12 }}><RegexView regexInfo={regexInfo} /></Grid.Col>)}
|
||||
</Grid>
|
||||
}
|
||||
|
||||
{srv?<AddNewRegex opened={open} onClose={() => {setOpen(false);}} service={srv} />:null}
|
||||
<YesNoModal
|
||||
title='Are you sure to delete this service?'
|
||||
description={`You are going to delete the service '${serviceInfo.port}', causing the stopping of the firewall and deleting all the regex associated. This will cause the shutdown of your service! ⚠️`}
|
||||
onClose={()=>setDeleteModal(false) }
|
||||
action={deleteService}
|
||||
opened={deleteModal}
|
||||
/>
|
||||
<RenameForm
|
||||
onClose={()=>setRenameModal(false)}
|
||||
opened={renameModal}
|
||||
service={serviceInfo}
|
||||
/>
|
||||
<AddEditService
|
||||
opened={editModal}
|
||||
onClose={()=>setEditModal(false)}
|
||||
edit={serviceInfo}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,100 +1,100 @@
|
||||
import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg, BsRegex } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFRegex/ServiceRow';
|
||||
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaFilter } from 'react-icons/fa';
|
||||
import { FaServer } from "react-icons/fa6";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
function NFRegex({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfregexServiceQuery()
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFRegex Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Add a new regex" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfregex" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfregex/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter Regex allows you to filter traffic using regexes</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start a service, add your regexes and it's already done!</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfregex" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{srv?
|
||||
<AddNewRegex opened={open} onClose={closeModal} service={srv} />:
|
||||
<AddEditService opened={open} onClose={closeModal} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
export default NFRegex;
|
||||
import { ActionIcon, Badge, Box, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg, BsRegex } from "react-icons/bs";
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
import ServiceRow from '../../components/NFRegex/ServiceRow';
|
||||
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddEditService from '../../components/NFRegex/AddEditService';
|
||||
import AddNewRegex from '../../components/AddNewRegex';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaFilter } from 'react-icons/fa';
|
||||
import { FaServer } from "react-icons/fa6";
|
||||
import { VscRegex } from "react-icons/vsc";
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
function NFRegex({ children }: { children: any }) {
|
||||
|
||||
const navigator = useNavigate()
|
||||
const [open, setOpen] = useState(false);
|
||||
const {srv} = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
const services = nfregexServiceQuery()
|
||||
|
||||
useEffect(()=> {
|
||||
if(services.isError)
|
||||
errorNotify("NFRegex Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='grape' ><BsRegex size={20} /></ThemeIcon><Space w="xs" />Netfilter Regex</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex' >
|
||||
{isMedium?"General stats:":null}
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="green" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge color="yellow" radius="sm" size="md" variant="filled"><FaFilter style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_packets, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
<Badge size="md" radius="sm" color="violet" variant="filled"><VscRegex style={{ marginBottom: -2, marginRight: 4}} />{services.isLoading?0:services.data?.reduce((acc, s)=> acc+=s.n_regex, 0)}</Badge>
|
||||
<Space w="xs" />
|
||||
</Box>
|
||||
{isMedium?null:<Space h="md" />}
|
||||
<Box className='center-flex' >
|
||||
{ srv?
|
||||
<Tooltip label="Add a new regex" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
: <Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["nfregex"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="nfregex" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
{srv?null:<>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data?.length > 0)?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} onClick={()=>{
|
||||
navigator("/nfregex/"+srv.service_id)
|
||||
}} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Netfilter Regex allows you to filter traffic using regexes</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start a service, add your regexes and it's already done!</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="nfregex" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
</>}
|
||||
</Box>
|
||||
{srv?children:null}
|
||||
{srv?
|
||||
<AddNewRegex opened={open} onClose={closeModal} service={srv} />:
|
||||
<AddEditService opened={open} onClose={closeModal} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
export default NFRegex;
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import ServiceRow from '../../components/PortHijack/ServiceRow';
|
||||
import { porthijackServiceQuery } from '../../components/PortHijack/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddNewService from '../../components/PortHijack/AddNewService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaServer } from 'react-icons/fa';
|
||||
import { GrDirections } from 'react-icons/gr';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
function PortHijack() {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const services = porthijackServiceQuery()
|
||||
|
||||
useEffect(()=>{
|
||||
if(services.isError)
|
||||
errorNotify("Porthijack Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="porthijack" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="porthijack" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
<AddNewService opened={open} onClose={closeModal} />
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
|
||||
export default PortHijack;
|
||||
import { ActionIcon, Badge, Box, Divider, LoadingOverlay, Space, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BsPlusLg } from "react-icons/bs";
|
||||
import ServiceRow from '../../components/PortHijack/ServiceRow';
|
||||
import { porthijackServiceQuery } from '../../components/PortHijack/utils';
|
||||
import { errorNotify, getErrorMessage, isMediumScreen } from '../../js/utils';
|
||||
import AddNewService from '../../components/PortHijack/AddNewService';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { TbReload } from 'react-icons/tb';
|
||||
import { FaServer } from 'react-icons/fa';
|
||||
import { GrDirections } from 'react-icons/gr';
|
||||
import { DocsButton } from '../../components/DocsButton';
|
||||
|
||||
|
||||
function PortHijack() {
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const queryClient = useQueryClient()
|
||||
const isMedium = isMediumScreen()
|
||||
|
||||
const services = porthijackServiceQuery()
|
||||
|
||||
useEffect(()=>{
|
||||
if(services.isError)
|
||||
errorNotify("Porthijack Update failed!", getErrorMessage(services.error))
|
||||
},[services.isError])
|
||||
|
||||
const closeModal = () => {setOpen(false);}
|
||||
|
||||
return <>
|
||||
<Space h="sm" />
|
||||
<Box className={isMedium?'center-flex':'center-flex-row'}>
|
||||
<Title order={5} className="center-flex"><ThemeIcon radius="md" size="md" variant='filled' color='blue' ><GrDirections size={20} /></ThemeIcon><Space w="xs" />Hijack port to proxy</Title>
|
||||
{isMedium?<Box className='flex-spacer' />:<Space h="sm" />}
|
||||
<Box className='center-flex'>
|
||||
<Badge size="md" radius="sm" color="yellow" variant="filled"><FaServer style={{ marginBottom: -1, marginRight: 4}} />Services: {services.isLoading?0:services.data?.length}</Badge>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Add a new service" position='bottom' color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="lg" radius="md" variant="filled"><BsPlusLg size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<Tooltip label="Refresh" position='bottom' color="indigo">
|
||||
<ActionIcon color="indigo" onClick={()=>queryClient.invalidateQueries(["porthijack"])} size="lg" radius="md" variant="filled"
|
||||
loading={services.isFetching}><TbReload size={18} /></ActionIcon>
|
||||
</Tooltip>
|
||||
<Space w="xs" />
|
||||
<DocsButton doc="porthijack" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Space h="md" />
|
||||
<Box className="center-flex-row" style={{gap: 20}}>
|
||||
<LoadingOverlay visible={services.isLoading} />
|
||||
{(services.data && services.data.length > 0) ?services.data.map( srv => <ServiceRow service={srv} key={srv.service_id} />):<>
|
||||
<Box className='center-flex-row'>
|
||||
<Space h="xl" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={3}>Hjiack Port to Proxy is a feature that allows you to run your custom proxy without touch the service config</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>It hijack the traffic to a secondary port, where you can run your proxy, that will still be able to contact the original service using loopback</Title>
|
||||
<Space h="xs" />
|
||||
<Title className='center-flex' style={{textAlign:"center"}} order={5}>Start using port hijacking creating a new service and routing the traffic to your proxy not changing the original service configs</Title>
|
||||
<Space h="lg" />
|
||||
<Box className='center-flex' style={{gap: 20}}>
|
||||
<Tooltip label="Add a new service" color="blue">
|
||||
<ActionIcon color="blue" onClick={()=>setOpen(true)} size="xl" radius="md" variant="filled">
|
||||
<BsPlusLg size="20px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<DocsButton doc="porthijack" size="xl" />
|
||||
</Box>
|
||||
</Box>
|
||||
</>}
|
||||
<AddNewService opened={open} onClose={closeModal} />
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
|
||||
export default PortHijack;
|
||||
|
||||
278
frontend/src/pages/Setup/index.tsx
Normal file
278
frontend/src/pages/Setup/index.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import { Box, Button, Code, Divider, FileButton, Group, List, Paper, Space, Stack, Text, Textarea, ThemeIcon, Title } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
import { FaCheck, FaDownload, FaExclamationTriangle, FaTimes, FaUpload } from 'react-icons/fa';
|
||||
import { MdSettings } from 'react-icons/md';
|
||||
import { getapi, isMediumScreen, postapi } from '../../js/utils';
|
||||
import { errorNotify, okNotify } from '../../js/utils';
|
||||
|
||||
export default function SetupPage() {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [importResult, setImportResult] = useState<any>(null);
|
||||
const [configJson, setConfigJson] = useState('');
|
||||
const isMedium = isMediumScreen();
|
||||
|
||||
const handleExport = async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const response = await getapi('/setup/export');
|
||||
const blob = new Blob([JSON.stringify(response, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `firegex-setup-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
okNotify('Configuration exported successfully', '');
|
||||
} catch (err) {
|
||||
errorNotify('Failed to export configuration', String(err));
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportFile = async () => {
|
||||
if (!file) {
|
||||
errorNotify('Please select a file first', '');
|
||||
return;
|
||||
}
|
||||
|
||||
setImporting(true);
|
||||
setImportResult(null);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch('/api/setup/import/file', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.detail || 'Import failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
setImportResult(result);
|
||||
|
||||
if (result.status === 'ok') {
|
||||
okNotify('Configuration imported successfully', '');
|
||||
} else {
|
||||
errorNotify('Configuration imported with errors', 'Check the results below');
|
||||
}
|
||||
} catch (err) {
|
||||
errorNotify('Failed to import configuration', String(err));
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportJson = async () => {
|
||||
if (!configJson.trim()) {
|
||||
errorNotify('Please enter a JSON configuration', '');
|
||||
return;
|
||||
}
|
||||
|
||||
setImporting(true);
|
||||
setImportResult(null);
|
||||
|
||||
try {
|
||||
const config = JSON.parse(configJson);
|
||||
const result = await postapi('/setup/import', config);
|
||||
setImportResult(result);
|
||||
|
||||
if (result.status === 'ok') {
|
||||
okNotify('Configuration imported successfully', '');
|
||||
} else {
|
||||
errorNotify('Configuration imported with errors', 'Check the results below');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof SyntaxError) {
|
||||
errorNotify('Invalid JSON format', String(err));
|
||||
} else {
|
||||
errorNotify('Failed to import configuration', String(err));
|
||||
}
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box px="md" mt="lg">
|
||||
<Title order={1} className="center-flex">
|
||||
<ThemeIcon radius="md" size="lg" variant='filled' color='cyan'>
|
||||
<MdSettings size={24} />
|
||||
</ThemeIcon>
|
||||
<Space w="sm" />
|
||||
Setup Import/Export
|
||||
</Title>
|
||||
<Text c="dimmed" mt="sm">
|
||||
Import or export your Firegex configuration including all services and rules
|
||||
</Text>
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
<Stack gap="xl">
|
||||
{/* Export Section */}
|
||||
<Paper shadow="sm" p="lg" withBorder>
|
||||
<Title order={3} mb="md">Export Configuration</Title>
|
||||
<Text c="dimmed" mb="md">
|
||||
Download all current services and rules as a JSON file
|
||||
</Text>
|
||||
<Button
|
||||
leftSection={<FaDownload />}
|
||||
onClick={handleExport}
|
||||
loading={exporting}
|
||||
color="teal"
|
||||
size="md"
|
||||
>
|
||||
Export to JSON
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
{/* Import from File Section */}
|
||||
<Paper shadow="sm" p="lg" withBorder>
|
||||
<Title order={3} mb="md">Import from File</Title>
|
||||
<Text c="dimmed" mb="md">
|
||||
Upload a setup.json file to create services and rules
|
||||
</Text>
|
||||
<Group>
|
||||
<FileButton onChange={setFile} accept="application/json">
|
||||
{(props) => (
|
||||
<Button {...props} variant="outline" color="cyan">
|
||||
{file ? file.name : 'Select JSON File'}
|
||||
</Button>
|
||||
)}
|
||||
</FileButton>
|
||||
<Button
|
||||
leftSection={<FaUpload />}
|
||||
onClick={handleImportFile}
|
||||
loading={importing}
|
||||
disabled={!file}
|
||||
color="blue"
|
||||
size="md"
|
||||
>
|
||||
Import from File
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
{/* Import from JSON Section */}
|
||||
<Paper shadow="sm" p="lg" withBorder>
|
||||
<Title order={3} mb="md">Import from JSON</Title>
|
||||
<Text c="dimmed" mb="md">
|
||||
Paste a JSON configuration directly
|
||||
</Text>
|
||||
<Textarea
|
||||
placeholder='{"services": [], "porthijack": [], "firewall": []}'
|
||||
value={configJson}
|
||||
onChange={(e) => setConfigJson(e.currentTarget.value)}
|
||||
minRows={10}
|
||||
maxRows={20}
|
||||
mb="md"
|
||||
styles={{ input: { fontFamily: 'monospace', fontSize: '12px' } }}
|
||||
/>
|
||||
<Button
|
||||
leftSection={<FaUpload />}
|
||||
onClick={handleImportJson}
|
||||
loading={importing}
|
||||
disabled={!configJson.trim()}
|
||||
color="blue"
|
||||
size="md"
|
||||
>
|
||||
Import from JSON
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
{/* Import Results */}
|
||||
{importResult && (
|
||||
<Paper shadow="sm" p="lg" withBorder>
|
||||
<Title order={3} mb="md">
|
||||
<Group>
|
||||
{importResult.status === 'ok' ? (
|
||||
<ThemeIcon color="teal" size="lg" radius="xl">
|
||||
<FaCheck />
|
||||
</ThemeIcon>
|
||||
) : (
|
||||
<ThemeIcon color="yellow" size="lg" radius="xl">
|
||||
<FaExclamationTriangle />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
Import Results
|
||||
</Group>
|
||||
</Title>
|
||||
|
||||
<Stack gap="md">
|
||||
<Group>
|
||||
<Text fw={500}>Services:</Text>
|
||||
<Text c={importResult.services_created > 0 ? "teal" : "dimmed"}>
|
||||
{importResult.services_created} created
|
||||
</Text>
|
||||
</Group>
|
||||
<Group>
|
||||
<Text fw={500}>PortHijack Services:</Text>
|
||||
<Text c={importResult.porthijack_created > 0 ? "teal" : "dimmed"}>
|
||||
{importResult.porthijack_created} created
|
||||
</Text>
|
||||
</Group>
|
||||
<Group>
|
||||
<Text fw={500}>Firewall Rules:</Text>
|
||||
<Text c={importResult.firewall_created > 0 ? "teal" : "dimmed"}>
|
||||
{importResult.firewall_created} created
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{importResult.errors && importResult.errors.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<Text fw={500} c="red">Errors:</Text>
|
||||
<List
|
||||
spacing="xs"
|
||||
size="sm"
|
||||
icon={
|
||||
<ThemeIcon color="red" size={20} radius="xl">
|
||||
<FaTimes size={12} />
|
||||
</ThemeIcon>
|
||||
}
|
||||
>
|
||||
{importResult.errors.map((error: string, idx: number) => (
|
||||
<List.Item key={idx}>
|
||||
<Code>{error}</Code>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Example Configuration */}
|
||||
<Paper shadow="sm" p="lg" withBorder>
|
||||
<Title order={3} mb="md">Example Configuration</Title>
|
||||
<Text c="dimmed" mb="md">
|
||||
Here's an example of the JSON structure:
|
||||
</Text>
|
||||
<Code block>{`{
|
||||
"services": [
|
||||
{
|
||||
"name": "Example HTTP Service",
|
||||
"port": 8080,
|
||||
"proto": "http",
|
||||
"ip_int": "0.0.0.0",
|
||||
"fail_open": true
|
||||
}
|
||||
],
|
||||
"porthijack": [],
|
||||
"firewall": []
|
||||
}`}</Code>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
307
frontend/src/pages/TrafficViewer/index.tsx
Normal file
307
frontend/src/pages/TrafficViewer/index.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import { ActionIcon, Badge, Box, Card, Divider, Group, LoadingOverlay, Select, Space, Text, TextInput, ThemeIcon, Title, Tooltip } from '@mantine/core';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { nfproxyServiceQuery } from '../../components/NFProxy/utils';
|
||||
import { nfregexServiceQuery } from '../../components/NFRegex/utils';
|
||||
import { isMediumScreen } from '../../js/utils';
|
||||
import { MdDoubleArrow, MdVisibility } from 'react-icons/md';
|
||||
import { TbPlugConnected } from 'react-icons/tb';
|
||||
import { FaFilter, FaServer } from 'react-icons/fa';
|
||||
import { BsRegex } from 'react-icons/bs';
|
||||
import { useState } from 'react';
|
||||
|
||||
type UnifiedService = {
|
||||
service_id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
port: number;
|
||||
proto: string;
|
||||
ip_int: string;
|
||||
type: 'nfproxy' | 'nfregex';
|
||||
stats: {
|
||||
edited_packets?: number;
|
||||
blocked_packets?: number;
|
||||
n_packets?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default function TrafficViewer() {
|
||||
const nfproxyServices = nfproxyServiceQuery();
|
||||
const nfregexServices = nfregexServiceQuery();
|
||||
const navigate = useNavigate();
|
||||
const isMedium = isMediumScreen();
|
||||
const [filterText, setFilterText] = useState('');
|
||||
const [filterType, setFilterType] = useState<string | null>(null);
|
||||
const [filterProto, setFilterProto] = useState<string | null>(null);
|
||||
const [filterStatus, setFilterStatus] = useState<string | null>(null);
|
||||
|
||||
if (nfproxyServices.isLoading || nfregexServices.isLoading) {
|
||||
return <LoadingOverlay visible={true} />;
|
||||
}
|
||||
|
||||
// Combine services from both modules
|
||||
const allServices: UnifiedService[] = [
|
||||
...(nfproxyServices.data?.map(s => ({
|
||||
service_id: s.service_id,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
port: s.port,
|
||||
proto: s.proto,
|
||||
ip_int: s.ip_int,
|
||||
type: 'nfproxy' as const,
|
||||
stats: {
|
||||
edited_packets: s.edited_packets,
|
||||
blocked_packets: s.blocked_packets
|
||||
}
|
||||
})) || []),
|
||||
...(nfregexServices.data?.map(s => ({
|
||||
service_id: s.service_id,
|
||||
name: s.name,
|
||||
status: s.status,
|
||||
port: s.port,
|
||||
proto: s.proto,
|
||||
ip_int: s.ip_int,
|
||||
type: 'nfregex' as const,
|
||||
stats: {
|
||||
n_packets: s.n_packets
|
||||
}
|
||||
})) || [])
|
||||
];
|
||||
|
||||
// Apply filters
|
||||
const filteredServices = allServices.filter(service => {
|
||||
// Text filter
|
||||
if (filterText) {
|
||||
const search = filterText.toLowerCase();
|
||||
const matchesText = (
|
||||
service.name.toLowerCase().includes(search) ||
|
||||
service.service_id.toLowerCase().includes(search) ||
|
||||
service.port.toString().includes(search) ||
|
||||
service.ip_int.toLowerCase().includes(search)
|
||||
);
|
||||
if (!matchesText) return false;
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if (filterType && service.type !== filterType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Protocol filter
|
||||
if (filterProto && service.proto !== filterProto) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (filterStatus) {
|
||||
if (filterStatus === 'active' && service.status !== 'active') return false;
|
||||
if (filterStatus === 'stopped' && service.status === 'active') return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const activeServices = filteredServices.filter(s => s.status === 'active');
|
||||
const stoppedServices = filteredServices.filter(s => s.status !== 'active');
|
||||
|
||||
return <>
|
||||
<Box px="md" mt="lg">
|
||||
<Title order={1} className="center-flex">
|
||||
<ThemeIcon radius="md" size="lg" variant='filled' color='cyan'>
|
||||
<MdVisibility size={24} />
|
||||
</ThemeIcon>
|
||||
<Space w="sm" />
|
||||
Traffic Viewer
|
||||
</Title>
|
||||
<Text c="dimmed" mt="sm">
|
||||
Monitor live network traffic for NFProxy and NFRegex services
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
<Box px="md" mb="lg">
|
||||
<Group grow>
|
||||
<TextInput
|
||||
placeholder="Search by name, ID, port, or IP..."
|
||||
value={filterText}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFilterText(e.currentTarget.value)}
|
||||
leftSection={<FaFilter />}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Service Type"
|
||||
clearable
|
||||
value={filterType}
|
||||
onChange={setFilterType}
|
||||
data={[
|
||||
{ value: 'nfproxy', label: 'Netfilter Proxy' },
|
||||
{ value: 'nfregex', label: 'Netfilter Regex' }
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Protocol"
|
||||
clearable
|
||||
value={filterProto}
|
||||
onChange={setFilterProto}
|
||||
data={[
|
||||
{ value: 'tcp', label: 'TCP' },
|
||||
{ value: 'udp', label: 'UDP' },
|
||||
{ value: 'http', label: 'HTTP' }
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Status"
|
||||
clearable
|
||||
value={filterStatus}
|
||||
onChange={setFilterStatus}
|
||||
data={[
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'stopped', label: 'Stopped' }
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
{allServices.length === 0 ? (
|
||||
<Box px="md">
|
||||
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
|
||||
No services found
|
||||
</Title>
|
||||
<Space h="xs" />
|
||||
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
|
||||
Create services in Netfilter Proxy or Netfilter Regex to start monitoring traffic
|
||||
</Text>
|
||||
</Box>
|
||||
) : filteredServices.length === 0 ? (
|
||||
<Box px="md">
|
||||
<Title order={3} className='center-flex' style={{ textAlign: "center" }}>
|
||||
No services match your filters
|
||||
</Title>
|
||||
<Space h="xs" />
|
||||
<Text className='center-flex' style={{ textAlign: "center" }} c="dimmed">
|
||||
Try adjusting your filter criteria
|
||||
</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Box px="md">
|
||||
{activeServices.length > 0 && (
|
||||
<>
|
||||
<Title order={3} mb="md">
|
||||
<Badge color="teal" size="lg" mr="xs">Active</Badge>
|
||||
Running Services
|
||||
</Title>
|
||||
{activeServices.map(service => (
|
||||
<Card key={`${service.type}-${service.service_id}`} shadow="sm" padding="lg" radius="md" withBorder mb="md">
|
||||
<Group justify="space-between">
|
||||
<Box>
|
||||
<Group>
|
||||
<ThemeIcon
|
||||
color={service.type === 'nfproxy' ? 'lime' : 'grape'}
|
||||
variant="light"
|
||||
size="lg"
|
||||
>
|
||||
{service.type === 'nfproxy' ? (
|
||||
<TbPlugConnected size={20} />
|
||||
) : (
|
||||
<BsRegex size={20} />
|
||||
)}
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Group gap="xs">
|
||||
<Text fw={700} size="lg">{service.name}</Text>
|
||||
<Badge
|
||||
size="xs"
|
||||
color={service.type === 'nfproxy' ? 'lime' : 'grape'}
|
||||
variant="dot"
|
||||
>
|
||||
{service.type === 'nfproxy' ? 'Proxy' : 'Regex'}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Group gap="xs" mt={4}>
|
||||
<Badge color="cyan" size="sm">:{service.port}</Badge>
|
||||
<Badge color="violet" size="sm">{service.proto}</Badge>
|
||||
<Badge color="gray" size="sm">{service.ip_int}</Badge>
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
</Box>
|
||||
<Box>
|
||||
<Group>
|
||||
<Box style={{ textAlign: 'right' }}>
|
||||
{service.type === 'nfproxy' ? (
|
||||
<>
|
||||
<Badge color="orange" size="sm" mb={4}>
|
||||
{service.stats.edited_packets || 0} edited
|
||||
</Badge>
|
||||
<br />
|
||||
<Badge color="yellow" size="sm">
|
||||
{service.stats.blocked_packets || 0} blocked
|
||||
</Badge>
|
||||
</>
|
||||
) : (
|
||||
<Badge color="yellow" size="sm">
|
||||
{service.stats.n_packets || 0} blocked
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
<Tooltip label="View traffic">
|
||||
<ActionIcon
|
||||
color="cyan"
|
||||
size="xl"
|
||||
radius="md"
|
||||
variant="filled"
|
||||
onClick={() => navigate(`/${service.type}/${service.service_id}/traffic`)}
|
||||
>
|
||||
<MdDoubleArrow size="24px" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Box>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
<Space h="xl" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{stoppedServices.length > 0 && (
|
||||
<>
|
||||
<Title order={3} mb="md">
|
||||
<Badge color="red" size="lg" mr="xs">Stopped</Badge>
|
||||
Inactive Services
|
||||
</Title>
|
||||
{stoppedServices.map(service => (
|
||||
<Card key={service.service_id} shadow="sm" padding="lg" radius="md" withBorder mb="md" opacity={0.6}>
|
||||
<Group justify="space-between">
|
||||
<Box>
|
||||
<Group>
|
||||
<ThemeIcon color={service.type === 'nfproxy' ? 'lime' : 'grape'} variant="light" size="lg">
|
||||
{service.type === 'nfproxy' ? <TbPlugConnected size={18} /> : <BsRegex size={18} />}
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="lg" c="dimmed">{service.name}</Text>
|
||||
<Badge color="gray" size="sm">
|
||||
{service.type === 'nfproxy' ? 'Proxy' : 'Regex'}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Group gap="xs" mt={4}>
|
||||
<Badge color="gray" size="sm">:{service.port}</Badge>
|
||||
<Badge color="gray" size="sm">{service.proto}</Badge>
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text size="sm" c="dimmed">
|
||||
Start service to view traffic
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>;
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"types": ["vite/client", "vite-plugin-svgr/client", "node"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
|
||||
30
run.py
30
run.py
@@ -254,9 +254,7 @@ def is_linux():
|
||||
def get_web_interface_url():
|
||||
if args.socket_dir:
|
||||
return os.path.join(args.socket_dir, "firegex.sock")
|
||||
|
||||
# Per altre piattaforme, usiamo l'host configurato se non è 0.0.0.0
|
||||
# altrimenti usiamo localhost per evitare confusione
|
||||
|
||||
display_host = "localhost" if args.host == "0.0.0.0" else args.host
|
||||
return f"http://{display_host}:{args.port}"
|
||||
|
||||
@@ -270,14 +268,14 @@ 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}",
|
||||
f"HOST={args.host}",
|
||||
f"NTHREADS={args.threads}",
|
||||
*([f"PSW_HASH_SET={hash_psw(psw_set)}"] if psw_set else []),
|
||||
*([f"SOCKET_DIR=/run/firegex"] if args.socket_dir else [])
|
||||
*(["SOCKET_DIR=/run/firegex"] if args.socket_dir else [])
|
||||
],
|
||||
"volumes": [
|
||||
"firegex_data:/execute/db",
|
||||
@@ -325,7 +323,7 @@ def write_compose(skip_password = True):
|
||||
"container_name": "firegex",
|
||||
"build" if g.build else "image": "." if g.build else f"ghcr.io/pwnzer0tt1/firegex:{args.version}",
|
||||
"ports": [
|
||||
f"{args.host}:{args.port}:{args.port}"
|
||||
f"{'' if args.host == 'any' else args.host+':'}{args.port}:{args.port}"
|
||||
],
|
||||
"environment": [
|
||||
f"PORT={args.port}",
|
||||
@@ -600,6 +598,10 @@ def cleanup_standalone_mounts():
|
||||
f"{g.rootfs_path}/sys_host/net.ipv6.conf.all.forwarding"
|
||||
]
|
||||
|
||||
# Add socket directory mount point if configured
|
||||
if args.socket_dir:
|
||||
mount_points.append(f"{g.rootfs_path}/run/firegex")
|
||||
|
||||
# Create umount commands (with || true to ignore errors)
|
||||
umount_commands = [f"umount -l {mount_point} || true" for mount_point in mount_points]
|
||||
|
||||
@@ -754,6 +756,18 @@ def setup_standalone_mounts():
|
||||
f"mount --bind /proc/sys/net/ipv6/conf/all/forwarding {g.rootfs_path}/sys_host/net.ipv6.conf.all.forwarding"
|
||||
])
|
||||
|
||||
# Add socket directory bind mount if configured
|
||||
if args.socket_dir:
|
||||
# Create socket directory on host if it doesn't exist
|
||||
# Create mount point in rootfs and bind mount the socket directory
|
||||
privileged_commands.extend([
|
||||
f"mkdir -p {args.socket_dir}",
|
||||
f"chmod 755 {args.socket_dir}",
|
||||
f"mkdir -p {g.rootfs_path}/run/firegex",
|
||||
f"chmod 755 {g.rootfs_path}/run/firegex",
|
||||
f"mount --bind {args.socket_dir} {g.rootfs_path}/run/firegex"
|
||||
])
|
||||
|
||||
# Run all privileged commands in one batch
|
||||
if not run_privileged_commands(privileged_commands, "setup bind mounts"):
|
||||
puts("Failed to set up bind mounts", color=colors.red)
|
||||
@@ -784,9 +798,9 @@ def run_standalone():
|
||||
if psw_set:
|
||||
env_vars.append(f"PSW_HASH_SET={hash_psw(psw_set)}")
|
||||
|
||||
# Add socket dir if set
|
||||
# Add socket dir if set (use path inside chroot)
|
||||
if args.socket_dir:
|
||||
env_vars.append(f"SOCKET_DIR={args.socket_dir}")
|
||||
env_vars.append("SOCKET_DIR=/run/firegex")
|
||||
|
||||
# Prepare environment string for chroot
|
||||
env_string = " ".join([f"{var}" for var in env_vars])
|
||||
|
||||
20
setup.example.json
Normal file
20
setup.example.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"name": "Example HTTP Service",
|
||||
"port": 8080,
|
||||
"proto": "http",
|
||||
"ip_int": "0.0.0.0",
|
||||
"fail_open": true
|
||||
},
|
||||
{
|
||||
"name": "Example TCP Service",
|
||||
"port": 443,
|
||||
"proto": "tcp",
|
||||
"ip_int": "0.0.0.0",
|
||||
"fail_open": false
|
||||
}
|
||||
],
|
||||
"porthijack": [],
|
||||
"firewall": []
|
||||
}
|
||||
@@ -4,92 +4,126 @@ from utils.firegexapi import FiregexAPI
|
||||
import argparse
|
||||
import secrets
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/")
|
||||
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password')
|
||||
args = parser.parse_args()
|
||||
sep()
|
||||
puts("Testing will start on ", color=colors.cyan, end="")
|
||||
puts(f"{args.address}", color=colors.yellow)
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--address",
|
||||
"-a",
|
||||
type=str,
|
||||
required=False,
|
||||
help="Address of firegex backend",
|
||||
default="http://127.0.0.1:4444/",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--password", "-p", type=str, required=True, help="Firegex password"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
sep()
|
||||
puts("Testing will start on ", color=colors.cyan, end="")
|
||||
puts(f"{args.address}", color=colors.yellow)
|
||||
|
||||
firegex = FiregexAPI(args.address)
|
||||
firegex = FiregexAPI(args.address)
|
||||
|
||||
#Connect to Firegex
|
||||
if firegex.status()["status"] == "init":
|
||||
if (firegex.set_password(args.password)):
|
||||
puts(f"Sucessfully set password to {args.password} ✔", color=colors.green)
|
||||
# Connect to Firegex
|
||||
if firegex.status()["status"] == "init":
|
||||
if firegex.set_password(args.password):
|
||||
puts(f"Sucessfully set password to {args.password} ✔", color=colors.green)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: Unknown response or password already put ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit(1)
|
||||
else:
|
||||
puts("Test Failed: Unknown response or password already put ✗", color=colors.red)
|
||||
exit(1)
|
||||
else:
|
||||
if (firegex.login(args.password)):
|
||||
puts("Sucessfully logged in ✔", color=colors.green)
|
||||
if firegex.login(args.password):
|
||||
puts("Sucessfully logged in ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
if firegex.status()["loggined"]:
|
||||
puts("Correctly received status ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||
puts("Test Failed: Unknown response or not logged in✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
if(firegex.status()["loggined"]):
|
||||
puts("Correctly received status ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Unknown response or not logged in✗", color=colors.red)
|
||||
exit(1)
|
||||
# Prepare second instance
|
||||
firegex2 = FiregexAPI(args.address)
|
||||
if firegex2.login(args.password):
|
||||
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: Unknown response or wrong passowrd on second instance ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit(1)
|
||||
|
||||
#Prepare second instance
|
||||
firegex2 = FiregexAPI(args.address)
|
||||
if (firegex2.login(args.password)):
|
||||
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red)
|
||||
exit(1)
|
||||
if firegex2.status()["loggined"]:
|
||||
puts("Correctly received status on second instance✔", color=colors.green)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: Unknown response or not logged in on second instance✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit(1)
|
||||
|
||||
if(firegex2.status()["loggined"]):
|
||||
puts("Correctly received status on second instance✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Unknown response or not logged in on second instance✗", color=colors.red)
|
||||
exit(1)
|
||||
# Change password
|
||||
new_password = secrets.token_hex(10)
|
||||
if firegex.change_password(new_password, expire=True):
|
||||
puts(f"Sucessfully changed password to {new_password} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
#Change password
|
||||
new_password = secrets.token_hex(10)
|
||||
if (firegex.change_password(new_password,expire=True)):
|
||||
puts(f"Sucessfully changed password to {new_password} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
||||
exit(1)
|
||||
# Check if we are still logged in
|
||||
if firegex.status()["loggined"]:
|
||||
puts("Correctly received status after password change ✔", color=colors.green)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: Unknown response or not logged after password change ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit(1)
|
||||
|
||||
#Check if we are still logged in
|
||||
if(firegex.status()["loggined"]):
|
||||
puts("Correctly received status after password change ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Unknown response or not logged after password change ✗", color=colors.red)
|
||||
exit(1)
|
||||
# Check if second session expired and relog
|
||||
|
||||
#Check if second session expired and relog
|
||||
if not firegex2.status()["loggined"]:
|
||||
puts("Second instance was expired currectly ✔", color=colors.green)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: Still logged in on second instance, expire expected ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit(1)
|
||||
if firegex2.login(new_password):
|
||||
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: Unknown response or wrong passowrd on second instance ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit(1)
|
||||
|
||||
if(not firegex2.status()["loggined"]):
|
||||
puts("Second instance was expired currectly ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Still logged in on second instance, expire expected ✗", color=colors.red)
|
||||
exit(1)
|
||||
if (firegex2.login(new_password)):
|
||||
puts("Sucessfully logged in on second instance ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Unknown response or wrong passowrd on second instance ✗", color=colors.red)
|
||||
exit(1)
|
||||
# Change it back
|
||||
if firegex.change_password(args.password, expire=False):
|
||||
puts("Sucessfully restored the password ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
#Change it back
|
||||
if (firegex.change_password(args.password,expire=False)):
|
||||
puts("Sucessfully restored the password ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coundl't change the password ✗", color=colors.red)
|
||||
exit(1)
|
||||
# Check if we are still logged in
|
||||
if firegex2.status()["loggined"]:
|
||||
puts("Correctly received status after password change ✔", color=colors.green)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: Unknown response or not logged after password change ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit(1)
|
||||
|
||||
#Check if we are still logged in
|
||||
if(firegex2.status()["loggined"]):
|
||||
puts("Correctly received status after password change ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Unknown response or not logged after password change ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
puts("List of available interfaces:", color=colors.yellow)
|
||||
for interface in firegex.get_interfaces():
|
||||
puts("name: {}, address: {}".format(interface["name"], interface["addr"]), color=colors.yellow)
|
||||
puts("List of available interfaces:", color=colors.yellow)
|
||||
for interface in firegex.get_interfaces():
|
||||
puts(
|
||||
"name: {}, address: {}".format(interface["name"], interface["addr"]),
|
||||
color=colors.yellow,
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,247 +8,366 @@ import secrets
|
||||
import base64
|
||||
import time
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/")
|
||||
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password')
|
||||
parser.add_argument("--service_name", "-n", type=str , required=False, help='Name of the test service', default="Test Service")
|
||||
parser.add_argument("--port", "-P", type=int , required=False, help='Port of the test service', default=1337)
|
||||
parser.add_argument("--ipv6", "-6" , action="store_true", help='Test Ipv6', default=False)
|
||||
parser.add_argument("--proto", "-m" , type=str, required=False, choices=["tcp","udp"], help='Select the protocol', default="tcp")
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--address",
|
||||
"-a",
|
||||
type=str,
|
||||
required=False,
|
||||
help="Address of firegex backend",
|
||||
default="http://127.0.0.1:4444/",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--password", "-p", type=str, required=True, help="Firegex password"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--service_name",
|
||||
"-n",
|
||||
type=str,
|
||||
required=False,
|
||||
help="Name of the test service",
|
||||
default="Test Service",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
"-P",
|
||||
type=int,
|
||||
required=False,
|
||||
help="Port of the test service",
|
||||
default=1337,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ipv6", "-6", action="store_true", help="Test Ipv6", default=False
|
||||
)
|
||||
parser.add_argument(
|
||||
"--proto",
|
||||
"-m",
|
||||
type=str,
|
||||
required=False,
|
||||
choices=["tcp", "udp"],
|
||||
help="Select the protocol",
|
||||
default="tcp",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
sep()
|
||||
puts("Testing will start on ", color=colors.cyan, end="")
|
||||
puts(f"{args.address}", color=colors.yellow)
|
||||
args = parser.parse_args()
|
||||
sep()
|
||||
puts("Testing will start on ", color=colors.cyan, end="")
|
||||
puts(f"{args.address}", color=colors.yellow)
|
||||
|
||||
firegex = FiregexAPI(args.address)
|
||||
firegex = FiregexAPI(args.address)
|
||||
|
||||
#Login
|
||||
if (firegex.login(args.password)):
|
||||
puts("Sucessfully logged in ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
#Create server
|
||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port,ipv6=args.ipv6)
|
||||
|
||||
def exit_test(code):
|
||||
if service_id:
|
||||
server.stop()
|
||||
if(firegex.nfregex_delete_service(service_id)):
|
||||
puts("Sucessfully deleted service ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
exit(code)
|
||||
|
||||
srvs = firegex.nfregex_get_services()
|
||||
for ele in srvs:
|
||||
if ele['name'] == args.service_name:
|
||||
firegex.nfregex_delete_service(ele['service_id'])
|
||||
|
||||
service_id = firegex.nfregex_add_service(args.service_name, args.port, args.proto , "::1" if args.ipv6 else "127.0.0.1" )
|
||||
if service_id:
|
||||
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
if(firegex.nfregex_start_service(service_id)):
|
||||
puts("Sucessfully started service ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
server.start()
|
||||
time.sleep(0.5)
|
||||
try:
|
||||
if server.sendCheckData(secrets.token_bytes(432)):
|
||||
puts("Successfully tested first proxy with no regex ✔", color=colors.green)
|
||||
# Login
|
||||
if firegex.login(args.password):
|
||||
puts("Sucessfully logged in ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Data was corrupted ", color=colors.red)
|
||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
# Create server
|
||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(
|
||||
args.port, ipv6=args.ipv6
|
||||
)
|
||||
|
||||
def exit_test(code):
|
||||
if service_id:
|
||||
server.stop()
|
||||
if firegex.nfregex_delete_service(service_id):
|
||||
puts("Sucessfully deleted service ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
exit(code)
|
||||
|
||||
srvs = firegex.nfregex_get_services()
|
||||
for ele in srvs:
|
||||
if ele["name"] == args.service_name:
|
||||
firegex.nfregex_delete_service(ele["service_id"])
|
||||
|
||||
service_id = firegex.nfregex_add_service(
|
||||
args.service_name, args.port, args.proto, "::1" if args.ipv6 else "127.0.0.1"
|
||||
)
|
||||
if service_id:
|
||||
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
if firegex.nfregex_start_service(service_id):
|
||||
puts("Sucessfully started service ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
except Exception:
|
||||
puts("Test Failed: Couldn't send data to the server ", color=colors.red)
|
||||
exit_test(1)
|
||||
#Add new regex
|
||||
secret = bytes(secrets.token_hex(16).encode())
|
||||
|
||||
if firegex.nfregex_add_regex(service_id,secret,"B",active=True,is_case_sensitive=True):
|
||||
puts(f"Sucessfully added regex {str(secret)} ✔", color=colors.green)
|
||||
else:
|
||||
puts(f"Test Failed: Couldn't add the regex {str(secret)} ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
server.start()
|
||||
time.sleep(0.5)
|
||||
try:
|
||||
if server.sendCheckData(secrets.token_bytes(432)):
|
||||
puts("Successfully tested first proxy with no regex ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Data was corrupted ", color=colors.red)
|
||||
exit_test(1)
|
||||
except Exception:
|
||||
puts("Test Failed: Couldn't send data to the server ", color=colors.red)
|
||||
exit_test(1)
|
||||
# Add new regex
|
||||
secret = bytes(secrets.token_hex(16).encode())
|
||||
|
||||
if firegex.nfregex_add_regex(
|
||||
service_id, secret, "B", active=True, is_case_sensitive=True
|
||||
):
|
||||
puts(f"Sucessfully added regex {str(secret)} ✔", color=colors.green)
|
||||
else:
|
||||
puts(f"Test Failed: Couldn't add the regex {str(secret)} ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
#Check if regex is present in the service
|
||||
n_blocked = 0
|
||||
# Check if regex is present in the service
|
||||
n_blocked = 0
|
||||
|
||||
def getMetric(metric_name, regex):
|
||||
for metric in firegex.nfregex_get_metrics().split("\n"):
|
||||
if metric.startswith(metric_name + "{") and f'regex="{regex}"' in metric:
|
||||
return int(metric.split(" ")[-1])
|
||||
def getMetric(metric_name, regex):
|
||||
for metric in firegex.nfregex_get_metrics().split("\n"):
|
||||
if metric.startswith(metric_name + "{") and f'regex="{regex}"' in metric:
|
||||
return int(metric.split(" ")[-1])
|
||||
|
||||
def checkRegex(regex, should_work=True, upper=False, deleted=False):
|
||||
if should_work:
|
||||
def checkRegex(regex, should_work=True, upper=False, deleted=False):
|
||||
if should_work:
|
||||
global n_blocked
|
||||
for r in firegex.nfregex_get_service_regexes(service_id):
|
||||
if r["regex"] == secret:
|
||||
# Test the regex
|
||||
s = regex.upper() if upper else regex
|
||||
if not server.sendCheckData(
|
||||
secrets.token_bytes(40) + s + secrets.token_bytes(40)
|
||||
):
|
||||
puts(
|
||||
"The malicious request was successfully blocked ✔",
|
||||
color=colors.green,
|
||||
)
|
||||
n_blocked += 1
|
||||
time.sleep(1)
|
||||
if firegex.nfregex_get_regex(r["id"])["n_packets"] == n_blocked:
|
||||
puts(
|
||||
"The packet was reported as blocked in the API ✔",
|
||||
color=colors.green,
|
||||
)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: The packet wasn't reported as blocked in the API ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit_test(1)
|
||||
if (
|
||||
getMetric("firegex_blocked_packets", secret.decode())
|
||||
== n_blocked
|
||||
):
|
||||
puts(
|
||||
"The packet was reported as blocked in the metrics ✔",
|
||||
color=colors.green,
|
||||
)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: The packet wasn't reported as blocked in the metrics ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit_test(1)
|
||||
if getMetric("firegex_active", secret.decode()) == 1:
|
||||
puts(
|
||||
"The regex was reported as active in the metrics ✔",
|
||||
color=colors.green,
|
||||
)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: The regex wasn't reported as active in the metrics ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit_test(1)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: The request wasn't blocked ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit_test(1)
|
||||
return
|
||||
puts("Test Failed: The regex wasn't found ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
else:
|
||||
if server.sendCheckData(
|
||||
secrets.token_bytes(40)
|
||||
+ base64.b64decode(regex)
|
||||
+ secrets.token_bytes(40)
|
||||
):
|
||||
puts("The request wasn't blocked ✔", color=colors.green)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: The request was blocked when it shouldn't have",
|
||||
color=colors.red,
|
||||
)
|
||||
exit_test(1)
|
||||
if not deleted:
|
||||
if getMetric("firegex_active", secret.decode()) == 0:
|
||||
puts(
|
||||
"The regex was reported as inactive in the metrics ✔",
|
||||
color=colors.green,
|
||||
)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: The regex wasn't reported as inactive in the metrics ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit_test(1)
|
||||
|
||||
def clear_regexes():
|
||||
global n_blocked
|
||||
n_blocked = 0
|
||||
for r in firegex.nfregex_get_service_regexes(service_id):
|
||||
if r["regex"] == secret:
|
||||
#Test the regex
|
||||
s = regex.upper() if upper else regex
|
||||
if not server.sendCheckData(secrets.token_bytes(40) + s + secrets.token_bytes(40)):
|
||||
puts("The malicious request was successfully blocked ✔", color=colors.green)
|
||||
n_blocked += 1
|
||||
time.sleep(1)
|
||||
if firegex.nfregex_get_regex(r["id"])["n_packets"] == n_blocked:
|
||||
puts("The packet was reported as blocked in the API ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: The packet wasn't reported as blocked in the API ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
if getMetric("firegex_blocked_packets", secret.decode()) == n_blocked:
|
||||
puts("The packet was reported as blocked in the metrics ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: The packet wasn't reported as blocked in the metrics ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
if getMetric("firegex_active", secret.decode()) == 1:
|
||||
puts("The regex was reported as active in the metrics ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: The regex wasn't reported as active in the metrics ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
if firegex.nfregex_delete_regex(r["id"]):
|
||||
puts(
|
||||
f"Sucessfully deleted regex with id {r['id']} ✔",
|
||||
color=colors.green,
|
||||
)
|
||||
else:
|
||||
puts("Test Failed: The request wasn't blocked ✗", color=colors.red)
|
||||
puts("Test Failed: Coulnd't delete the regex ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
return
|
||||
puts("Test Failed: The regex wasn't found ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
else:
|
||||
if server.sendCheckData(secrets.token_bytes(40) + base64.b64decode(regex) + secrets.token_bytes(40)):
|
||||
puts("The request wasn't blocked ✔", color=colors.green)
|
||||
break
|
||||
if f'regex="{secret.decode()}"' not in firegex.nfregex_get_metrics():
|
||||
puts("No regex metrics after deletion ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: The request was blocked when it shouldn't have", color=colors.red)
|
||||
puts(
|
||||
"Test Failed: Metrics found after deleting the regex ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit_test(1)
|
||||
if not deleted:
|
||||
if getMetric("firegex_active", secret.decode()) == 0:
|
||||
puts("The regex was reported as inactive in the metrics ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: The regex wasn't reported as inactive in the metrics ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
def clear_regexes():
|
||||
global n_blocked
|
||||
n_blocked = 0
|
||||
checkRegex(secret)
|
||||
|
||||
# Pause the proxy
|
||||
if firegex.nfregex_stop_service(service_id):
|
||||
puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't pause the service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
# Check if it's actually paused
|
||||
checkRegex(secret, should_work=False)
|
||||
|
||||
# Start firewall
|
||||
if firegex.nfregex_start_service(service_id):
|
||||
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
checkRegex(secret)
|
||||
|
||||
# Disable regex
|
||||
for r in firegex.nfregex_get_service_regexes(service_id):
|
||||
if r["regex"] == secret:
|
||||
if(firegex.nfregex_delete_regex(r["id"])):
|
||||
puts(f"Sucessfully deleted regex with id {r['id']} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't delete the regex ✗", color=colors.red)
|
||||
if firegex.nfregex_disable_regex(r["id"]):
|
||||
puts(
|
||||
f"Sucessfully disabled regex with id {r['id']} ✔",
|
||||
color=colors.green,
|
||||
)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't disable the regex ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
break
|
||||
if f'regex="{secret.decode()}"' not in firegex.nfregex_get_metrics():
|
||||
puts("No regex metrics after deletion ✔", color=colors.green)
|
||||
|
||||
# Check if it's actually disabled
|
||||
checkRegex(secret, should_work=False)
|
||||
|
||||
# Enable regex
|
||||
for r in firegex.nfregex_get_service_regexes(service_id):
|
||||
if r["regex"] == secret:
|
||||
if firegex.nfregex_enable_regex(r["id"]):
|
||||
puts(
|
||||
f"Sucessfully enabled regex with id {r['id']} ✔", color=colors.green
|
||||
)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't enable the regex ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
break
|
||||
|
||||
checkRegex(secret)
|
||||
|
||||
# Delete regex
|
||||
clear_regexes()
|
||||
|
||||
# Check if it's actually deleted
|
||||
checkRegex(secret, should_work=False, deleted=True)
|
||||
|
||||
# Add case insensitive regex
|
||||
if firegex.nfregex_add_regex(
|
||||
service_id, secret, "B", active=True, is_case_sensitive=False
|
||||
):
|
||||
puts(
|
||||
f"Sucessfully added case insensitive regex {str(secret)} ✔",
|
||||
color=colors.green,
|
||||
)
|
||||
else:
|
||||
puts("Test Failed: Metrics found after deleting the regex ✗", color=colors.red)
|
||||
puts(
|
||||
f"Test Failed: Coulnd't add the case insensitive regex {str(secret)} ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit_test(1)
|
||||
|
||||
checkRegex(secret)
|
||||
checkRegex(secret, upper=True)
|
||||
checkRegex(secret)
|
||||
|
||||
#Pause the proxy
|
||||
if(firegex.nfregex_stop_service(service_id)):
|
||||
puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't pause the service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
clear_regexes()
|
||||
|
||||
#Check if it's actually paused
|
||||
checkRegex(secret,should_work=False)
|
||||
|
||||
#Start firewall
|
||||
if(firegex.nfregex_start_service(service_id)):
|
||||
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
checkRegex(secret)
|
||||
|
||||
#Disable regex
|
||||
for r in firegex.nfregex_get_service_regexes(service_id):
|
||||
if r["regex"] == secret:
|
||||
if(firegex.nfregex_disable_regex(r["id"])):
|
||||
puts(f"Sucessfully disabled regex with id {r['id']} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't disable the regex ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
break
|
||||
|
||||
#Check if it's actually disabled
|
||||
checkRegex(secret,should_work=False)
|
||||
|
||||
#Enable regex
|
||||
for r in firegex.nfregex_get_service_regexes(service_id):
|
||||
if r["regex"] == secret:
|
||||
if(firegex.nfregex_enable_regex(r["id"])):
|
||||
puts(f"Sucessfully enabled regex with id {r['id']} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't enable the regex ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
break
|
||||
|
||||
checkRegex(secret)
|
||||
|
||||
#Delete regex
|
||||
clear_regexes()
|
||||
|
||||
#Check if it's actually deleted
|
||||
checkRegex(secret,should_work=False,deleted=True)
|
||||
|
||||
#Add case insensitive regex
|
||||
if(firegex.nfregex_add_regex(service_id,secret,"B",active=True, is_case_sensitive=False)):
|
||||
puts(f"Sucessfully added case insensitive regex {str(secret)} ✔", color=colors.green)
|
||||
else:
|
||||
puts(f"Test Failed: Coulnd't add the case insensitive regex {str(secret)} ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
checkRegex(secret, upper=True)
|
||||
checkRegex(secret)
|
||||
|
||||
clear_regexes()
|
||||
|
||||
#Rename service
|
||||
if(firegex.nfregex_rename_service(service_id,f"{args.service_name}2")):
|
||||
puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
#Check if service was renamed correctly
|
||||
service = firegex.nfregex_get_service(service_id)
|
||||
if service["name"] == f"{args.service_name}2":
|
||||
puts("Checked that service was renamed correctly ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
#Rename back service
|
||||
if(firegex.nfregex_rename_service(service_id,f"{args.service_name}")):
|
||||
puts(f"Sucessfully renamed service to {args.service_name} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
#Change settings
|
||||
opposite_proto = "udp" if args.proto == "tcp" else "tcp"
|
||||
if(firegex.nfregex_settings_service(service_id, 1338, opposite_proto, "::dead:beef" if args.ipv6 else "123.123.123.123", True)):
|
||||
srv_updated = firegex.nfregex_get_service(service_id)
|
||||
if srv_updated["port"] == 1338 and srv_updated["proto"] == opposite_proto and ("::dead:beef" if args.ipv6 else "123.123.123.123") in srv_updated["ip_int"] and srv_updated["fail_open"]:
|
||||
puts("Sucessfully changed service settings ✔", color=colors.green)
|
||||
# Rename service
|
||||
if firegex.nfregex_rename_service(service_id, f"{args.service_name}2"):
|
||||
puts(
|
||||
f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green
|
||||
)
|
||||
else:
|
||||
puts("Test Failed: Service settings weren't updated correctly ✗", color=colors.red)
|
||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
exit_test(0)
|
||||
# Check if service was renamed correctly
|
||||
service = firegex.nfregex_get_service(service_id)
|
||||
if service["name"] == f"{args.service_name}2":
|
||||
puts("Checked that service was renamed correctly ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
# Rename back service
|
||||
if firegex.nfregex_rename_service(service_id, f"{args.service_name}"):
|
||||
puts(
|
||||
f"Sucessfully renamed service to {args.service_name} ✔", color=colors.green
|
||||
)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
# Change settings
|
||||
opposite_proto = "udp" if args.proto == "tcp" else "tcp"
|
||||
if firegex.nfregex_settings_service(
|
||||
service_id,
|
||||
1338,
|
||||
opposite_proto,
|
||||
"::dead:beef" if args.ipv6 else "123.123.123.123",
|
||||
True,
|
||||
):
|
||||
srv_updated = firegex.nfregex_get_service(service_id)
|
||||
if (
|
||||
srv_updated["port"] == 1338
|
||||
and srv_updated["proto"] == opposite_proto
|
||||
and ("::dead:beef" if args.ipv6 else "123.123.123.123")
|
||||
in srv_updated["ip_int"]
|
||||
and srv_updated["fail_open"]
|
||||
):
|
||||
puts("Sucessfully changed service settings ✔", color=colors.green)
|
||||
else:
|
||||
puts(
|
||||
"Test Failed: Service settings weren't updated correctly ✗",
|
||||
color=colors.red,
|
||||
)
|
||||
exit_test(1)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't change service settings ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
exit_test(0)
|
||||
|
||||
289
tests/ph_test.py
289
tests/ph_test.py
@@ -7,131 +7,180 @@ import argparse
|
||||
import secrets
|
||||
import time
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--address", "-a", type=str , required=False, help='Address of firegex backend', default="http://127.0.0.1:4444/")
|
||||
parser.add_argument("--password", "-p", type=str, required=True, help='Firegex password')
|
||||
parser.add_argument("--service_name", "-n", type=str , required=False, help='Name of the test service', default="Test Service")
|
||||
parser.add_argument("--port", "-P", type=int , required=False, help='Port of the test service', default=1337)
|
||||
parser.add_argument("--ipv6", "-6" , action="store_true", help='Test Ipv6', default=False)
|
||||
parser.add_argument("--proto", "-m" , type=str, required=False, choices=["tcp","udp"], help='Select the protocol', default="tcp")
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--address",
|
||||
"-a",
|
||||
type=str,
|
||||
required=False,
|
||||
help="Address of firegex backend",
|
||||
default="http://127.0.0.1:4444/",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--password", "-p", type=str, required=True, help="Firegex password"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--service_name",
|
||||
"-n",
|
||||
type=str,
|
||||
required=False,
|
||||
help="Name of the test service",
|
||||
default="Test Service",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
"-P",
|
||||
type=int,
|
||||
required=False,
|
||||
help="Port of the test service",
|
||||
default=1337,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ipv6", "-6", action="store_true", help="Test Ipv6", default=False
|
||||
)
|
||||
parser.add_argument(
|
||||
"--proto",
|
||||
"-m",
|
||||
type=str,
|
||||
required=False,
|
||||
choices=["tcp", "udp"],
|
||||
help="Select the protocol",
|
||||
default="tcp",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
sep()
|
||||
puts("Testing will start on ", color=colors.cyan, end="")
|
||||
puts(f"{args.address}", color=colors.yellow)
|
||||
args = parser.parse_args()
|
||||
sep()
|
||||
puts("Testing will start on ", color=colors.cyan, end="")
|
||||
puts(f"{args.address}", color=colors.yellow)
|
||||
|
||||
firegex = FiregexAPI(args.address)
|
||||
firegex = FiregexAPI(args.address)
|
||||
|
||||
#Login
|
||||
if (firegex.login(args.password)):
|
||||
puts("Sucessfully logged in ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
#Create server
|
||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port+1,ipv6=args.ipv6,proxy_port=args.port)
|
||||
|
||||
def exit_test(code):
|
||||
if service_id:
|
||||
server.stop()
|
||||
if(firegex.ph_delete_service(service_id)):
|
||||
puts("Sucessfully deleted service ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
exit(code)
|
||||
|
||||
srvs = firegex.ph_get_services()
|
||||
for ele in srvs:
|
||||
if ele['name'] == args.service_name:
|
||||
firegex.ph_delete_service(ele['service_id'])
|
||||
|
||||
#Create and start serivce
|
||||
service_id = firegex.ph_add_service(args.service_name, args.port, args.port+1, args.proto , "::1" if args.ipv6 else "127.0.0.1", "::1" if args.ipv6 else "127.0.0.1")
|
||||
if service_id:
|
||||
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
if(firegex.ph_start_service(service_id)):
|
||||
puts("Sucessfully started service ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
server.start()
|
||||
time.sleep(0.5)
|
||||
|
||||
#Check if it started
|
||||
def checkData(should_work):
|
||||
res = None
|
||||
try:
|
||||
res = server.sendCheckData(secrets.token_bytes(432))
|
||||
except (ConnectionRefusedError, TimeoutError):
|
||||
res = None
|
||||
if res:
|
||||
if should_work:
|
||||
puts("Successfully received data ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Connection wasn't blocked ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
# Login
|
||||
if firegex.login(args.password):
|
||||
puts("Sucessfully logged in ✔", color=colors.green)
|
||||
else:
|
||||
if should_work:
|
||||
puts("Test Failed: Data wans't received ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
puts("Test Failed: Unknown response or wrong passowrd ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
# Create server
|
||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(
|
||||
args.port + 1, ipv6=args.ipv6, proxy_port=args.port
|
||||
)
|
||||
|
||||
def exit_test(code):
|
||||
if service_id:
|
||||
server.stop()
|
||||
if firegex.ph_delete_service(service_id):
|
||||
puts("Sucessfully deleted service ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't delete serivce ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
exit(code)
|
||||
|
||||
srvs = firegex.ph_get_services()
|
||||
for ele in srvs:
|
||||
if ele["name"] == args.service_name:
|
||||
firegex.ph_delete_service(ele["service_id"])
|
||||
|
||||
# Create and start serivce
|
||||
service_id = firegex.ph_add_service(
|
||||
args.service_name,
|
||||
args.port,
|
||||
args.port + 1,
|
||||
args.proto,
|
||||
"::1" if args.ipv6 else "127.0.0.1",
|
||||
"::1" if args.ipv6 else "127.0.0.1",
|
||||
)
|
||||
if service_id:
|
||||
puts(f"Sucessfully created service {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Failed to create service ✗", color=colors.red)
|
||||
exit(1)
|
||||
|
||||
if firegex.ph_start_service(service_id):
|
||||
puts("Sucessfully started service ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Failed to start service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
server.start()
|
||||
time.sleep(0.5)
|
||||
|
||||
# Check if it started
|
||||
def checkData(should_work):
|
||||
res = None
|
||||
try:
|
||||
res = server.sendCheckData(secrets.token_bytes(432))
|
||||
except (ConnectionRefusedError, TimeoutError):
|
||||
res = None
|
||||
if res:
|
||||
if should_work:
|
||||
puts("Successfully received data ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Connection wasn't blocked ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
else:
|
||||
puts("Successfully blocked connection ✔", color=colors.green)
|
||||
if should_work:
|
||||
puts("Test Failed: Data wans't received ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
else:
|
||||
puts("Successfully blocked connection ✔", color=colors.green)
|
||||
|
||||
checkData(True)
|
||||
checkData(True)
|
||||
|
||||
#Pause the proxy
|
||||
if(firegex.ph_stop_service(service_id)):
|
||||
puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't pause the service ✗", color=colors.red)
|
||||
# Pause the proxy
|
||||
if firegex.ph_stop_service(service_id):
|
||||
puts(f"Sucessfully paused service with id {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't pause the service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
checkData(False)
|
||||
|
||||
# Start firewall
|
||||
if firegex.ph_start_service(service_id):
|
||||
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
checkData(True)
|
||||
|
||||
# Change port
|
||||
if firegex.ph_change_destination(
|
||||
service_id, "::1" if args.ipv6 else "127.0.0.1", args.port + 2
|
||||
):
|
||||
puts("Sucessfully changed port ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't change destination ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
checkData(False)
|
||||
|
||||
server.stop()
|
||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(
|
||||
args.port + 2, ipv6=args.ipv6, proxy_port=args.port
|
||||
)
|
||||
server.start()
|
||||
time.sleep(0.5)
|
||||
|
||||
checkData(True)
|
||||
|
||||
# Rename service
|
||||
if firegex.ph_rename_service(service_id, f"{args.service_name}2"):
|
||||
puts(
|
||||
f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green
|
||||
)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
# Check if service was renamed correctly
|
||||
for services in firegex.ph_get_services():
|
||||
if services["name"] == f"{args.service_name}2":
|
||||
puts("Checked that service was renamed correctly ✔", color=colors.green)
|
||||
exit_test(0)
|
||||
|
||||
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
checkData(False)
|
||||
|
||||
#Start firewall
|
||||
if(firegex.ph_start_service(service_id)):
|
||||
puts(f"Sucessfully started service with id {service_id} ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't start the service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
checkData(True)
|
||||
|
||||
#Change port
|
||||
if(firegex.ph_change_destination(service_id, "::1" if args.ipv6 else "127.0.0.1", args.port+2)):
|
||||
puts("Sucessfully changed port ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't change destination ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
checkData(False)
|
||||
|
||||
server.stop()
|
||||
server = (TcpServer if args.proto == "tcp" else UdpServer)(args.port+2,ipv6=args.ipv6,proxy_port=args.port)
|
||||
server.start()
|
||||
time.sleep(0.5)
|
||||
|
||||
checkData(True)
|
||||
|
||||
#Rename service
|
||||
if(firegex.ph_rename_service(service_id,f"{args.service_name}2")):
|
||||
puts(f"Sucessfully renamed service to {args.service_name}2 ✔", color=colors.green)
|
||||
else:
|
||||
puts("Test Failed: Coulnd't rename service ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
#Check if service was renamed correctly
|
||||
for services in firegex.ph_get_services():
|
||||
if services["name"] == f"{args.service_name}2":
|
||||
puts("Checked that service was renamed correctly ✔", color=colors.green)
|
||||
exit_test(0)
|
||||
|
||||
puts("Test Failed: Service wasn't renamed correctly ✗", color=colors.red)
|
||||
exit_test(1)
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
0,4090.616
|
||||
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
|
||||
|
||||
|
@@ -8,7 +8,7 @@ ERROR=0
|
||||
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
until curl --output /dev/null --silent --fail http://localhost:4444/api/status; do
|
||||
until curl --output /dev/null --silent --fail http://127.0.0.1:4444/api/status; do
|
||||
printf '.'
|
||||
sleep 1
|
||||
done
|
||||
|
||||
@@ -1,9 +1,46 @@
|
||||
import queue
|
||||
from multiprocessing import Process, Queue
|
||||
import socket
|
||||
import traceback
|
||||
|
||||
|
||||
def _start_tcp_server(port, server_queue: Queue, ipv6, verbose):
|
||||
sock = socket.socket(
|
||||
socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_STREAM
|
||||
)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("::1" if ipv6 else "127.0.0.1", port))
|
||||
sock.listen(8)
|
||||
while True:
|
||||
connection, address = sock.accept()
|
||||
while True:
|
||||
try:
|
||||
buf = connection.recv(4096)
|
||||
if buf == b"":
|
||||
break
|
||||
|
||||
reply = buf # Default to echo
|
||||
try:
|
||||
# See if there is a custom reply, but don't block
|
||||
custom_reply = server_queue.get(block=False)
|
||||
reply = custom_reply
|
||||
except queue.Empty:
|
||||
pass # No custom reply, just echo
|
||||
|
||||
if verbose:
|
||||
print("SERVER: ", reply)
|
||||
connection.sendall(reply)
|
||||
except (ConnectionResetError, BrokenPipeError):
|
||||
break # Client closed connection
|
||||
except Exception:
|
||||
if verbose:
|
||||
traceback.print_exc()
|
||||
break # Exit on other errors
|
||||
connection.close()
|
||||
|
||||
|
||||
class TcpServer:
|
||||
def __init__(self,port,ipv6,proxy_port=None, verbose=False):
|
||||
def __init__(self, port, ipv6, proxy_port=None, verbose=False):
|
||||
self.proxy_port = proxy_port
|
||||
self.ipv6 = ipv6
|
||||
self.port = port
|
||||
@@ -12,43 +49,30 @@ class TcpServer:
|
||||
self._regen_process()
|
||||
|
||||
def _regen_process(self):
|
||||
def _startServer(port, server_queue:Queue):
|
||||
sock = socket.socket(socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(('::1' if self.ipv6 else '127.0.0.1', port))
|
||||
sock.listen(8)
|
||||
while True:
|
||||
connection,address = sock.accept()
|
||||
while True:
|
||||
try:
|
||||
buf = connection.recv(4096)
|
||||
if buf == b'':
|
||||
break
|
||||
try:
|
||||
buf = server_queue.get(block=False)
|
||||
except Exception:
|
||||
pass
|
||||
if self.verbose:
|
||||
print("SERVER: ", buf)
|
||||
connection.sendall(buf)
|
||||
except Exception:
|
||||
if self.verbose:
|
||||
traceback.print_exc()
|
||||
connection.close()
|
||||
self.server = Process(target=_startServer,args=[self.port, self._server_data_queue])
|
||||
self.server = Process(
|
||||
target=_start_tcp_server,
|
||||
args=[self.port, self._server_data_queue, self.ipv6, self.verbose],
|
||||
)
|
||||
|
||||
def start(self):
|
||||
self.server.start()
|
||||
|
||||
|
||||
def stop(self):
|
||||
self.server.terminate()
|
||||
self.server.join()
|
||||
self._regen_process()
|
||||
|
||||
def connect_client(self):
|
||||
self.client_sock = socket.socket(socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.client_sock = socket.socket(
|
||||
socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_STREAM
|
||||
)
|
||||
self.client_sock.settimeout(1)
|
||||
self.client_sock.connect(('::1' if self.ipv6 else '127.0.0.1', self.proxy_port if self.proxy_port else self.port))
|
||||
self.client_sock.connect(
|
||||
(
|
||||
"::1" if self.ipv6 else "127.0.0.1",
|
||||
self.proxy_port if self.proxy_port else self.port,
|
||||
)
|
||||
)
|
||||
|
||||
def close_client(self):
|
||||
if self.client_sock:
|
||||
@@ -60,7 +84,7 @@ class TcpServer:
|
||||
if server_reply:
|
||||
self._server_data_queue.put(server_reply)
|
||||
self.client_sock.sendall(packet)
|
||||
|
||||
|
||||
def recv_packet(self):
|
||||
try:
|
||||
return self.client_sock.recv(4096)
|
||||
@@ -68,7 +92,7 @@ class TcpServer:
|
||||
if self.verbose:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def sendCheckData(self, data, get_data=False):
|
||||
self.connect_client()
|
||||
self.send_packet(data)
|
||||
|
||||
@@ -1,35 +1,94 @@
|
||||
from multiprocessing import Process
|
||||
from multiprocessing import Process, Queue
|
||||
import socket
|
||||
import queue
|
||||
import traceback
|
||||
|
||||
|
||||
def _start_udp_server(port, server_queue: Queue, ipv6, verbose):
|
||||
sock = socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("::1" if ipv6 else "127.0.0.1", port))
|
||||
while True:
|
||||
try:
|
||||
bytesAddressPair = sock.recvfrom(4096)
|
||||
message = bytesAddressPair[0]
|
||||
address = bytesAddressPair[1]
|
||||
|
||||
reply = message # Default to echo
|
||||
try:
|
||||
# See if there is a custom reply, but don't block
|
||||
custom_reply = server_queue.get(block=False)
|
||||
reply = custom_reply
|
||||
except queue.Empty:
|
||||
pass # No custom reply, just echo
|
||||
|
||||
if verbose:
|
||||
print(f"SERVER: sending {reply} to {address}")
|
||||
sock.sendto(reply, address)
|
||||
except Exception:
|
||||
if verbose:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class UdpServer:
|
||||
def __init__(self,port,ipv6, proxy_port = None):
|
||||
def _startServer(port):
|
||||
sock = socket.socket(socket.AF_INET6 if ipv6 else socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(('::1' if ipv6 else '127.0.0.1', port))
|
||||
while True:
|
||||
bytesAddressPair = sock.recvfrom(432)
|
||||
message = bytesAddressPair[0]
|
||||
address = bytesAddressPair[1]
|
||||
sock.sendto(message, address)
|
||||
|
||||
self.ipv6 = ipv6
|
||||
def __init__(self, port, ipv6, proxy_port=None, verbose=False):
|
||||
self.port = port
|
||||
self.ipv6 = ipv6
|
||||
self.proxy_port = proxy_port
|
||||
self.server = Process(target=_startServer,args=[port])
|
||||
self.verbose = verbose
|
||||
self._server_data_queue = Queue()
|
||||
self._regen_process()
|
||||
|
||||
def _regen_process(self):
|
||||
self.server = Process(
|
||||
target=_start_udp_server,
|
||||
args=[self.port, self._server_data_queue, self.ipv6, self.verbose],
|
||||
)
|
||||
|
||||
def start(self):
|
||||
self.server.start()
|
||||
|
||||
|
||||
def stop(self):
|
||||
self.server.terminate()
|
||||
self.server.join()
|
||||
self._regen_process()
|
||||
|
||||
def sendCheckData(self,data):
|
||||
s = socket.socket(socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.settimeout(2)
|
||||
s.sendto(data, ('::1' if self.ipv6 else '127.0.0.1', self.proxy_port if self.proxy_port else self.port))
|
||||
def connect_client(self):
|
||||
self.client_sock = socket.socket(
|
||||
socket.AF_INET6 if self.ipv6 else socket.AF_INET, socket.SOCK_DGRAM
|
||||
)
|
||||
self.client_sock.settimeout(1)
|
||||
self.client_sock.connect(
|
||||
(
|
||||
"::1" if self.ipv6 else "127.0.0.1",
|
||||
self.proxy_port if self.proxy_port else self.port,
|
||||
)
|
||||
)
|
||||
|
||||
def close_client(self):
|
||||
if self.client_sock:
|
||||
self.client_sock.close()
|
||||
|
||||
def send_packet(self, packet, server_reply=None):
|
||||
if self.verbose:
|
||||
print("CLIENT: ", packet)
|
||||
if server_reply:
|
||||
self._server_data_queue.put(server_reply)
|
||||
self.client_sock.sendall(packet)
|
||||
|
||||
def recv_packet(self):
|
||||
try:
|
||||
received_data = s.recvfrom(432)
|
||||
except Exception:
|
||||
return self.client_sock.recv(4096)
|
||||
except (TimeoutError, ConnectionResetError):
|
||||
if self.verbose:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
return received_data[0] == data
|
||||
|
||||
def sendCheckData(self, data, get_data=False):
|
||||
self.connect_client()
|
||||
self.send_packet(data)
|
||||
received_data = self.recv_packet()
|
||||
self.close_client()
|
||||
if get_data:
|
||||
return received_data
|
||||
return received_data == data
|
||||
|
||||
Reference in New Issue
Block a user