diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 97caad4e2..4d2a3479d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -35,12 +35,12 @@ jobs: python -m pip install --no-cache-dir --require-hashes -r src/common/db/requirements.txt echo "CODEQL_PYTHON=$(which python)" >> $GITHUB_ENV - name: Initialize CodeQL - uses: github/codeql-action/init@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 + uses: github/codeql-action/init@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 with: languages: ${{ matrix.language }} config-file: ./.github/codeql.yml setup-python-dependencies: false - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 + uses: github/codeql-action/analyze@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml index cc03c0b22..5747a11bf 100644 --- a/.github/workflows/container-build.yml +++ b/.github/workflows/container-build.yml @@ -56,6 +56,8 @@ jobs: echo "$SSH_KEY" > ~/.ssh/id_rsa_arm chmod 600 ~/.ssh/id_rsa_arm echo "$SSH_CONFIG" | sed "s/SSH_IP/$SSH_IP/g" > ~/.ssh/config + echo "ServerAliveInterval 60" >> ~/.ssh/config + echo "ServerAliveCountMax 10" >> ~/.ssh/config env: SSH_KEY: ${{ secrets.ARM_SSH_KEY }} SSH_IP: ${{ secrets.ARM_SSH_IP }} @@ -84,7 +86,7 @@ jobs: # Compute metadata - name: Extract metadata id: meta - uses: docker/metadata-action@9dc751fe249ad99385a2583ee0d084c400eee04e # v5.4.0 + uses: docker/metadata-action@dbef88086f6cef02e264edb7dbf63250c17cef6c # v5.5.0 with: images: bunkerity/${{ inputs.IMAGE }} # Build cached image diff --git a/.github/workflows/create-arm.yml b/.github/workflows/create-arm.yml index e1f272c48..9bb50ff84 100644 --- a/.github/workflows/create-arm.yml +++ b/.github/workflows/create-arm.yml @@ -46,7 +46,7 @@ jobs: default-organization-id: ${{ secrets.SCW_DEFAULT_ORGANIZATION_ID }} - name: Extract ARM type run: | - TYPE=$(echo "$JSON" | jq '.servers | with_entries(select(.key | contains("AMP"))) | with_entries(select(.value.availability != "shortage")) | keys[] | select(. | test("^AMP2-C[0-9]+$")) | sub("AMP2-C"; "") | tonumber' | sort -n | tail -n 1 | xargs -I {} echo "AMP2-C{}") + TYPE=$(echo "$JSON" | jq '.servers | with_entries(select(.key | contains("COPARM1-"))) | with_entries(select(.value.availability != "shortage")) | keys[] | select(. | test("^COPARM1-[0-9]+C-[0-9]+G$"))' | sed 's/"//g' | cut -d '-' -f 2,3 | sort -g | tail -n 1 | xargs -I {} echo "COPARM1-{}") echo "Type is $TYPE" echo "TYPE=$TYPE" >> "$GITHUB_ENV" env: @@ -81,6 +81,6 @@ jobs: SSH_IP: ${{ fromJson(steps.scw.outputs.json).public_ip.address }} SSH_CONFIG: ${{ secrets.ARM_SSH_CONFIG }} - name: Install Docker - run: ssh root@$SSH_IP "curl -fsSL https://test.docker.com -o test-docker.sh ; sh test-docker.sh" + run: ssh root@$SSH_IP "curl -fsSL https://test.docker.com -o test-docker.sh ; sh test-docker.sh ; echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config ; echo 'ClientAliveCountMax 0' >> /etc/ssh/sshd_config ; systemctl restart ssh" env: SSH_IP: ${{ fromJson(steps.scw.outputs.json).public_ip.address }} diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index f5df172ac..adf4045b3 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -13,6 +13,7 @@ jobs: contents: read packages: write strategy: + fail-fast: false matrix: image: [bunkerweb, scheduler, autoconf, ui] include: @@ -84,7 +85,6 @@ jobs: # Core tests prepare-tests-core: - needs: [build-containers, build-packages] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -96,7 +96,7 @@ jobs: outputs: tests: ${{ steps.set-matrix.outputs.tests }} tests-core: - needs: prepare-tests-core + needs: [build-containers, prepare-tests-core] strategy: fail-fast: false matrix: @@ -106,7 +106,7 @@ jobs: TEST: ${{ matrix.test }} RELEASE: dev tests-core-linux: - needs: prepare-tests-core + needs: [build-packages, prepare-tests-core] strategy: fail-fast: false matrix: diff --git a/.github/workflows/doc-to-pdf.yml b/.github/workflows/doc-to-pdf.yml index f840d2ba2..ae5aac03a 100644 --- a/.github/workflows/doc-to-pdf.yml +++ b/.github/workflows/doc-to-pdf.yml @@ -18,8 +18,8 @@ jobs: uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: python-version: "3.10" - - name: Install doc requirements - run: pip install --no-cache-dir --require-hashes -r docs/requirements.txt + - name: Install doc dependencies + run: pip install --no-cache-dir --require-hashes -r docs/requirements.txt && sudo apt install -y libcairo2-dev libfreetype6-dev libffi-dev libjpeg-dev libpng-dev libz-dev - name: Install chromium run: sudo apt install chromium-browser - name: Install node @@ -32,7 +32,7 @@ jobs: run: mkdocs serve & sleep 10 - name: Run pdf script run: node docs/misc/pdf.js http://localhost:8000/print_page/ BunkerWeb_documentation_v${{ inputs.VERSION }}.pdf 'BunkerWeb documentation v${{ inputs.VERSION }}' - - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: BunkerWeb_documentation_v${{ inputs.VERSION }}.pdf path: BunkerWeb_documentation_v${{ inputs.VERSION }}.pdf diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 69109b98b..71c26d77f 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -65,6 +65,8 @@ jobs: echo "$SSH_KEY" > ~/.ssh/id_rsa_arm chmod 600 ~/.ssh/id_rsa_arm echo "$SSH_CONFIG" | sed "s/SSH_IP/$SSH_IP/g" > ~/.ssh/config + echo "ServerAliveInterval 60" >> ~/.ssh/config + echo "ServerAliveCountMax 10" >> ~/.ssh/config env: SSH_KEY: ${{ secrets.ARM_SSH_KEY }} SSH_IP: ${{ secrets.ARM_SSH_IP }} @@ -127,7 +129,7 @@ jobs: scp -r root@arm:/root/package-${{ inputs.LINUX }} ./package-${{ inputs.LINUX }} env: LARCH: ${{ env.LARCH }} - - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 with: name: package-${{ inputs.LINUX }}-${{ env.LARCH }} path: package-${{ inputs.LINUX }}/*.${{ inputs.PACKAGE }} @@ -135,7 +137,7 @@ jobs: - name: Extract metadata if: inputs.TEST == true id: meta - uses: docker/metadata-action@9dc751fe249ad99385a2583ee0d084c400eee04e # v5.4.0 + uses: docker/metadata-action@dbef88086f6cef02e264edb7dbf63250c17cef6c # v5.5.0 with: images: ghcr.io/bunkerity/${{ inputs.LINUX }}-tests:${{ inputs.RELEASE }} - name: Build test image diff --git a/.github/workflows/push-doc.yml b/.github/workflows/push-doc.yml index 6f7fbc317..319a11d3e 100644 --- a/.github/workflows/push-doc.yml +++ b/.github/workflows/push-doc.yml @@ -32,8 +32,8 @@ jobs: - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 with: python-version: "3.10" - - name: Install doc requirements - run: pip install --no-cache-dir --require-hashes -r docs/requirements.txt + - name: Install doc dependencies + run: pip install --no-cache-dir --require-hashes -r docs/requirements.txt && sudo apt install -y libcairo2-dev libfreetype6-dev libffi-dev libjpeg-dev libpng-dev libz-dev - name: Push doc run: mike deploy --update-aliases --push --alias-type=copy ${{ inputs.VERSION }} ${{ inputs.ALIAS }} - name: Set default doc diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index 05fa5d7be..ee576a2b7 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -51,6 +51,8 @@ jobs: echo "$SSH_KEY" > ~/.ssh/id_rsa_arm chmod 600 ~/.ssh/id_rsa_arm echo "$SSH_CONFIG" | sed "s/SSH_IP/$SSH_IP/g" > ~/.ssh/config + echo "ServerAliveInterval 60" >> ~/.ssh/config + echo "ServerAliveCountMax 10" >> ~/.ssh/config env: SSH_KEY: ${{ secrets.ARM_SSH_KEY }} SSH_IP: ${{ secrets.ARM_SSH_IP }} @@ -63,7 +65,7 @@ jobs: # Compute metadata - name: Extract metadata id: meta - uses: docker/metadata-action@9dc751fe249ad99385a2583ee0d084c400eee04e # v5.4.0 + uses: docker/metadata-action@dbef88086f6cef02e264edb7dbf63250c17cef6c # v5.5.0 with: images: bunkerity/${{ inputs.IMAGE }} # Build and push diff --git a/.github/workflows/push-github.yml b/.github/workflows/push-github.yml index 241b3be7a..4b7d9843e 100644 --- a/.github/workflows/push-github.yml +++ b/.github/workflows/push-github.yml @@ -19,7 +19,7 @@ jobs: # Get PDF doc - name: Get documentation if: inputs.VERSION != 'testing' - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: BunkerWeb_documentation_v${{ inputs.VERSION }}.pdf # Create tag diff --git a/.github/workflows/push-packagecloud.yml b/.github/workflows/push-packagecloud.yml index 5bcbb259d..2a2113256 100644 --- a/.github/workflows/push-packagecloud.yml +++ b/.github/workflows/push-packagecloud.yml @@ -42,18 +42,18 @@ jobs: - name: Check out repository code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install ruby - uses: ruby/setup-ruby@360dc864d5da99d54fcb8e9148c14a84b90d3e88 # v1.165.1 + uses: ruby/setup-ruby@5daca165445f0ae10478593083f72ca2625e241d # v1.169.0 with: ruby-version: "3.0" - name: Install packagecloud run: gem install package_cloud # Download packages - - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 if: inputs.LINUX != 'el' with: name: package-${{ inputs.LINUX }}-${{ inputs.PACKAGE_ARCH }} path: /tmp/${{ inputs.LINUX }} - - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 if: inputs.LINUX == 'el' with: name: package-rhel-${{ inputs.PACKAGE_ARCH }} diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index 1f897f919..1bcdc5320 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -25,6 +25,6 @@ jobs: results_format: sarif publish_results: true - name: "Upload SARIF results to code scanning" - uses: github/codeql-action/upload-sarif@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 + uses: github/codeql-action/upload-sarif@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 with: sarif_file: results.sarif diff --git a/.github/workflows/staging-create-infra.yml b/.github/workflows/staging-create-infra.yml index e6e121285..fac4f3087 100644 --- a/.github/workflows/staging-create-infra.yml +++ b/.github/workflows/staging-create-infra.yml @@ -55,7 +55,7 @@ jobs: if: always() env: SECRET_KEY: ${{ secrets.SECRET_KEY }} - - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + - uses: actions/upload-artifact@694cdabd8bdb0f10b2cea11669e1bf5453eed0a6 # v4.2.0 if: always() with: name: tf-${{ inputs.TYPE }} diff --git a/.github/workflows/staging-delete-infra.yml b/.github/workflows/staging-delete-infra.yml index ddca65457..021c78b1d 100644 --- a/.github/workflows/staging-delete-infra.yml +++ b/.github/workflows/staging-delete-infra.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install terraform uses: hashicorp/setup-terraform@a1502cd9e758c50496cc9ac5308c4843bcd56d36 # v3.0.0 - - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: tf-${{ inputs.TYPE }} path: /tmp diff --git a/.github/workflows/staging-tests.yml b/.github/workflows/staging-tests.yml index a51ff335b..4cc8dd436 100644 --- a/.github/workflows/staging-tests.yml +++ b/.github/workflows/staging-tests.yml @@ -43,7 +43,7 @@ jobs: if: inputs.TYPE == 'swarm' - name: Install test dependencies run: pip3 install --no-cache-dir --require-hashes --no-deps -r tests/requirements.txt - - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + - uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: tf-k8s path: /tmp diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 8b04007f0..a00d03d0c 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -85,7 +85,6 @@ jobs: SECRET_KEY: ${{ secrets.SECRET_KEY }} K8S_IP: ${{ secrets.K8S_IP }} prepare-tests-core: - needs: [codeql, build-containers, build-packages] runs-on: ubuntu-latest steps: - name: Checkout repository @@ -130,7 +129,7 @@ jobs: RUNS_ON: ${{ matrix.runs_on }} secrets: inherit tests-core: - needs: prepare-tests-core + needs: [build-containers, prepare-tests-core] strategy: fail-fast: false matrix: @@ -140,7 +139,7 @@ jobs: TEST: ${{ matrix.test }} RELEASE: testing tests-core-linux: - needs: prepare-tests-core + needs: [build-packages, prepare-tests-core] strategy: fail-fast: false matrix: diff --git a/.github/workflows/test-core-linux.yml b/.github/workflows/test-core-linux.yml index a8c190d6f..2a619c7f5 100644 --- a/.github/workflows/test-core-linux.yml +++ b/.github/workflows/test-core-linux.yml @@ -97,7 +97,7 @@ jobs: run: | export MAKEFLAGS="-j $(nproc)" pip install --no-cache-dir --ignore-installed --require-hashes -r src/deps/requirements-deps.txt - MAKEFLAGS="-j $(nproc)" find tests/core -name "requirements.txt" -exec pip install --no-cache-dir --require-hashes --no-deps -r {} \; - cd ./tests/core/${{ inputs.TEST }} + cd tests/core/${{ inputs.TEST }} + find . -name "requirements.txt" -exec pip install --no-cache-dir --require-hashes --no-deps -r {} \; sudo truncate -s 0 /var/log/bunkerweb/error.log ./test.sh "linux" diff --git a/.github/workflows/tests-ui-linux.yml b/.github/workflows/tests-ui-linux.yml index 8b59645d4..bf9ae398d 100644 --- a/.github/workflows/tests-ui-linux.yml +++ b/.github/workflows/tests-ui-linux.yml @@ -66,14 +66,13 @@ jobs: - name: Fix version without a starting number if: inputs.RELEASE == 'testing' || inputs.RELEASE == 'dev' || inputs.RELEASE == 'ui' run: echo "force-bad-version" | sudo tee -a /etc/dpkg/dpkg.cfg - - name: Install BunkerWeb - run: sudo apt install -fy /tmp/bunkerweb.deb - name: Edit configuration files run: | # Misc echo "127.0.0.1 www.example.com" | sudo tee -a /etc/hosts echo "127.0.0.1 app1.example.com" | sudo tee -a /etc/hosts # BunkerWeb + sudo mkdir -p /etc/bunkerweb echo "SERVER_NAME=" | sudo tee /etc/bunkerweb/variables.env echo "HTTP_PORT=80" | sudo tee -a /etc/bunkerweb/variables.env echo 'DNS_RESOLVERS=9.9.9.9 8.8.8.8 8.8.4.4' | sudo tee -a /etc/bunkerweb/variables.env @@ -92,6 +91,8 @@ jobs: sudo chown nginx:nginx /etc/bunkerweb/variables.env /etc/bunkerweb/ui.env sudo chmod 777 /etc/bunkerweb/variables.env /etc/bunkerweb/ui.env + - name: Install BunkerWeb + run: sudo apt install -fy /tmp/bunkerweb.deb - name: Run tests run: | export MAKEFLAGS="-j $(nproc)" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index baee1ba60..8ae7f6c38 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks -exclude: (^LICENSE.md$|^src/VERSION$|^env/|^src/(bw/misc/root-ca.pem$|deps/src/|common/core/modsecurity/files|ui/static/js/(editor/|utils/purify/|tsparticles\.bundle\.min\.js))|\.(svg|drawio|patch\d?|ascii|tf|tftpl|key)$) +exclude: (^LICENSE.md$|^src/VERSION$|^env/|^src/(bw/misc/root-ca.pem$|deps/src/|common/core/modsecurity/files|ui/static/(js/(editor/|utils/purify/|tsparticles\.bundle\.min\.js)|css/dashboard\.css))|\.(svg|drawio|patch\d?|ascii|tf|tftpl|key)$) repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: c4a0b883114b00d8d76b479c820ce7950211c99b # frozen: v4.5.0 @@ -30,7 +30,7 @@ repos: name: Prettier Code Formatter - repo: https://github.com/JohnnyMorganz/StyLua - rev: f9afc7f33bc19f7708fbc1d7eea0606e0d41080a # frozen: v0.19.1 + rev: 84c370104d6a8d1eef00c80a3ebd42f7033aaaad # frozen: v0.20.0 hooks: - id: stylua-github exclude: ^src/(bw/lua/middleclass.lua|common/core/antibot/captcha.lua)$ @@ -50,7 +50,7 @@ repos: args: ["--max-line-length=250", "--ignore=E266,E402,E722,W503"] - repo: https://github.com/dosisod/refurb - rev: a7c461fcfaa2ca3248d489cdf7fed8e2d4fd8520 # frozen: v1.26.0 + rev: a295cee6d188f5797aefe5d7cf77a353ed48ea93 # frozen: v1.27.0 hooks: - id: refurb name: Refurb Python Refactoring Tool @@ -62,7 +62,7 @@ repos: - id: codespell name: Codespell Spell Checker exclude: (^src/(ui/templates|common/core/.+/files|bw/loading)/.+.html|modsecurity-rules.conf.*)$ - entry: codespell --ignore-regex="(tabEl|Widgits)" --skip src/ui/static/js/utils/flatpickr.js,CHANGELOG.md + entry: codespell --ignore-regex="(tabEl|Widgits)" --skip src/ui/static/js/utils/flatpickr.js,src/ui/static/css/style.css,CHANGELOG.md language: python types: [text] diff --git a/.trivyignore b/.trivyignore index e69de29bb..db367b1cd 100644 --- a/.trivyignore +++ b/.trivyignore @@ -0,0 +1 @@ +CVE-2023-6129 diff --git a/CHANGELOG.md b/CHANGELOG.md index cf917ea7e..f3729cea1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,26 @@ # Changelog -## v1.5.5 - YYYY/MM/DD +## v1.5.6 - YYYY/MM/DD -- [BUGFIX] Fix issues with the database when upgrading from version 1.5.3 and 1.5.4 to the most recent version +- [MISC] Updated Linux base images in Dockerfiles +- [DEPS] Updated stream-lua-nginx-module to v0.0.14 +- [DEPS] Updated lua-nginx-module version to v0.10.26 +- [DEPS] Updated libmaxminddb version to v1.9.1 +- [DEPS] Updated lua-resty-core to v0.1.28 +- [DEPS] Updated zlib version to v1.3.1 + +## v1.5.5 - 2024/01/12 + +- [BUGFIX] Fix issues with the database when upgrading from one version to a newer one - [BUGFIX] Fix ModSecurity-nginx to make it work with brotli - [BUGFIX] Remove certbot renew delay causing errors on k8s - [BUGFIX] Fix missing custom modsec files when BW instances change - [BUGFIX] Fix inconsistency on config changes when using Redis +- [BUGFIX] Fix web UI not working when using / URL - [FEATURE] Add Anonymous reporting feature - [FEATURE] Add support for fallback Referrer-Policies -- [FEATURE] Add profile page to web ui and the possibility to activate the 2FA +- [FEATURE] Add 2FA support to web UI +- [FEATURE] Add username and password management to web UI - [FEATURE] Add setting REVERSE_PROXY_INCLUDES to manually add "include" directives in the reverse proxies - [FEATURE] Add support for Redis Sentinel - [FEATURE] Add support for tls in Ingress definition @@ -17,7 +28,6 @@ - [MISC] Various internal improvements in LUA code - [MISC] Check nginx configuration before reload - [MISC] Updated Python Docker image to 3.12.1-alpine3.18 in Dockerfiles -- [MISC] Switch gunicorn worker_class back to gevent in web UI - [DEPS] Updated ModSecurity to v3.0.11 ## v1.5.4 - 2023/12/04 diff --git a/README.md b/README.md index 714bbff8b..37ef44118 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- BunkerWeb logo + BunkerWeb logo

diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..176dbcd87 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "puppeteer": "^21.3.6" + } +} diff --git a/docs/requirements.in b/docs/requirements.in index 6c4eeadba..232e85712 100644 --- a/docs/requirements.in +++ b/docs/requirements.in @@ -1,5 +1,5 @@ mike==2.0.0 mkdocs==1.5.3 -mkdocs-material[imaging]==9.5.3 +mkdocs-material[imaging]==9.5.4 mkdocs-print-site-plugin==2.3.6 pytablewriter==1.2.0 diff --git a/docs/requirements.txt b/docs/requirements.txt index 8b5dacd8b..8c12ad156 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -211,81 +211,81 @@ importlib-resources==6.1.1 \ --hash=sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a \ --hash=sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6 # via mike -jinja2==3.1.2 \ - --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ - --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 +jinja2==3.1.3 \ + --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ + --hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90 # via # mike # mkdocs # mkdocs-material -markdown==3.5.1 \ - --hash=sha256:5874b47d4ee3f0b14d764324d2c94c03ea66bee56f2d929da9f2508d65e722dc \ - --hash=sha256:b65d7beb248dc22f2e8a31fb706d93798093c308dc1aba295aedeb9d41a813bd +markdown==3.5.2 \ + --hash=sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd \ + --hash=sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8 # via # mkdocs # mkdocs-material # pymdown-extensions -markupsafe==2.1.3 \ - --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ - --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ - --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ - --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ - --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \ - --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ - --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ - --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \ - --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \ - --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ - --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ - --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ - --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ - --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ - --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ - --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ - --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \ - --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ - --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ - --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ - --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ - --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ - --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ - --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ - --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ - --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \ - --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ - --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ - --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ - --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ - --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ - --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ - --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ - --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \ - --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ - --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ - --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \ - --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ - --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \ - --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ - --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ - --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ - --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ - --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ - --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ - --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ - --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ - --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ - --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ - --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ - --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ - --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ - --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ - --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ - --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ - --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ - --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ - --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \ - --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \ - --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11 +markupsafe==2.1.4 \ + --hash=sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69 \ + --hash=sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0 \ + --hash=sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d \ + --hash=sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec \ + --hash=sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5 \ + --hash=sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411 \ + --hash=sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3 \ + --hash=sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74 \ + --hash=sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0 \ + --hash=sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949 \ + --hash=sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d \ + --hash=sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279 \ + --hash=sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f \ + --hash=sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6 \ + --hash=sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc \ + --hash=sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e \ + --hash=sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954 \ + --hash=sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656 \ + --hash=sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc \ + --hash=sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518 \ + --hash=sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56 \ + --hash=sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc \ + --hash=sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa \ + --hash=sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565 \ + --hash=sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4 \ + --hash=sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb \ + --hash=sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250 \ + --hash=sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4 \ + --hash=sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959 \ + --hash=sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc \ + --hash=sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474 \ + --hash=sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863 \ + --hash=sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8 \ + --hash=sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f \ + --hash=sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2 \ + --hash=sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e \ + --hash=sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e \ + --hash=sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb \ + --hash=sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f \ + --hash=sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a \ + --hash=sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26 \ + --hash=sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d \ + --hash=sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2 \ + --hash=sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131 \ + --hash=sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789 \ + --hash=sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6 \ + --hash=sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a \ + --hash=sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858 \ + --hash=sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e \ + --hash=sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb \ + --hash=sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e \ + --hash=sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84 \ + --hash=sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7 \ + --hash=sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea \ + --hash=sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b \ + --hash=sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6 \ + --hash=sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475 \ + --hash=sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74 \ + --hash=sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a \ + --hash=sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00 # via # jinja2 # mkdocs @@ -311,9 +311,9 @@ mkdocs==1.5.3 \ # -r requirements.in # mike # mkdocs-material -mkdocs-material==9.5.3 \ - --hash=sha256:5899219f422f0a6de784232d9d40374416302ffae3c160cacc72969fcc1ee372 \ - --hash=sha256:76c93a8525cceb0b395b9cedab3428bf518cf6439adef2b940f1c1574b775d89 +mkdocs-material==9.5.4 \ + --hash=sha256:3d196ee67fad16b2df1a458d650a8ac1890294eaae368d26cee71bc24ad41c40 \ + --hash=sha256:efd7cc8ae03296d728da9bd38f4db8b07ab61f9738a0cbd0dfaf2a15a50e7343 # via # -r requirements.in # mkdocs-material @@ -477,6 +477,7 @@ pyyaml==6.0.1 \ --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ + --hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \ --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ diff --git a/docs/settings.md b/docs/settings.md index 59dfc5c36..c346266b8 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -550,4 +550,3 @@ Allow access based on internal and external IP/network/rDNS/ASN whitelists. |`WHITELIST_USER_AGENT_URLS`| |global |no |List of URLs, separated with spaces, containing good User-Agent to whitelist. | |`WHITELIST_URI` | |multisite|no |List of URI (PCRE regex), separated with spaces, to whitelist. | |`WHITELIST_URI_URLS` | |global |no |List of URLs, separated with spaces, containing bad URI to whitelist. | - diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 62a0aa736..ed966699a 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -465,4 +465,4 @@ In case you lost your UI credentials or have 2FA issues, you can connect to the 1|||0||(manual or ui) ``` - You should now be able to log into the web UI only using your username and password. \ No newline at end of file + You should now be able to log into the web UI only using your username and password. diff --git a/docs/web-ui.md b/docs/web-ui.md index e8f3e9eb8..9bce2944c 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -37,7 +37,9 @@ Because the web UI is a web application, the recommended installation procedure ## Setup wizard -The setup wizard is a feature that helps you to **configure** and **install the web UI** using a **user-friendly interface**. You will need to set the `UI_HOST` setting (`http://hostname-of-web-ui:7000`) and browse the `/setup` URI of your server to access the setup wizard. +!!! info "Wizard" + + The setup wizard is a feature that helps you to **configure** and **install the web UI** using a **user-friendly interface**. You will need to set the `UI_HOST` setting (`http://hostname-of-web-ui:7000`) and browse the `/setup` URI of your server to access the setup wizard.

![Overview](assets/img/ui-wizard-account.webp){ align=center, width="350" } @@ -66,6 +68,11 @@ Review your final BunkerWeb UI URL and then click on the `Setup` button. Once th If you want to use the setup wizard, you will need to set the `UI_HOST` setting to the HTTP endpoint of your web UI container. For example, if your web UI container is named `bw-ui` and is listening on the `7000` port, you will need to set the `UI_HOST` setting to `http://bw-ui:7000`. + !!! tip "Accessing the setup wizard" + + You can access the setup wizard by browsing the `http://your-ip-address/setup` URI of your server. + + Here is the docker-compose boilerplate that you can use (don't forget to edit the `changeme` data) : ```yaml @@ -153,6 +160,10 @@ Review your final BunkerWeb UI URL and then click on the `Setup` button. Once th If you want to use the setup wizard, you will need to set the `UI_HOST` setting to the HTTP endpoint of your web UI container. For example, if your web UI container is named `bw-ui` and is listening on the `7000` port, you will need to set the `UI_HOST` setting to `http://bw-ui:7000`. + !!! tip "Accessing the setup wizard" + + You can access the setup wizard by browsing the `http://your-ip-address/setup` URI of your server. + Here is the docker-compose boilerplate that you can use (don't forget to edit the `changeme` data) : ```yaml @@ -256,6 +267,10 @@ Review your final BunkerWeb UI URL and then click on the `Setup` button. Once th If you want to use the setup wizard, you will need to set the `UI_HOST` setting to the HTTP endpoint of your web UI container. For example, if your web UI container is named `bw-ui` and is listening on the `7000` port, you will need to set the `UI_HOST` setting to `http://bw-ui:7000`. + !!! tip "Accessing the setup wizard" + + You can access the setup wizard by browsing the `http://your-ip-address/setup` URI of your server. + Here is the stack boilerplate that you can use (don't forget to edit the `changeme` data) : ```yaml @@ -382,6 +397,10 @@ Review your final BunkerWeb UI URL and then click on the `Setup` button. Once th If you want to use the setup wizard, you will need to set the `UI_HOST` setting to the HTTP endpoint of your web UI SERVICE. For example, if your web UI service is named `svc-bunkerweb-ui` and is listening on the `7000` port, you will need to set the `UI_HOST` setting to `http://svc-bunkerweb-ui:7000`. + !!! tip "Accessing the setup wizard" + + You can access the setup wizard by browsing the `http://your-ip-address/setup` URI of your server. + Here is the yaml boilerplate that you can use (don't forget to edit the `changeme` data) : ```yaml @@ -695,6 +714,10 @@ Review your final BunkerWeb UI URL and then click on the `Setup` button. Once th If you want to use the setup wizard, you will need to set the `UI_HOST` setting to the HTTP endpoint of your web UI SERVICE. Since the web UI is listening on the same machine as BunkerWeb, you will need to set the `UI_HOST` setting `http://127.0.0.1:7000`. + !!! tip "Accessing the setup wizard" + + You can access the setup wizard by browsing the `http://your-ip-address/setup` URI of your server. + Here is the `/etc/bunkerweb/variables.env` boilerplate you can use : ```conf @@ -772,7 +795,7 @@ The following steps are needed to enable the TOTP feature from the web UI : - Enter your current password !!! info "Secret key refresh" - A new secret key is **generated each time** you visit the page or submit the form. In case something went wrong (e.g. : expired TOTP code), you will need to copy the new secret key to your authenticator app until 2FA is successfuly enabled. + A new secret key is **generated each time** you visit the page or submit the form. In case something went wrong (e.g. : expired TOTP code), you will need to copy the new secret key to your authenticator app until 2FA is successfully enabled. Once enabled, 2FA authentication can be disabled at the same place. diff --git a/examples/autoconf-configs/tests.json b/examples/autoconf-configs/tests.json index dc92c1d69..e58915d3a 100644 --- a/examples/autoconf-configs/tests.json +++ b/examples/autoconf-configs/tests.json @@ -1,7 +1,7 @@ { "name": "autoconf-configs", "kinds": ["autoconf"], - "delay": 60, + "delay": 180, "timeout": 60, "tests": [ { diff --git a/examples/docker-configs/tests.json b/examples/docker-configs/tests.json index c9bb815c2..ab3805887 100644 --- a/examples/docker-configs/tests.json +++ b/examples/docker-configs/tests.json @@ -1,7 +1,7 @@ { "name": "docker-configs", "kinds": ["docker"], - "delay": 30, + "delay": 120, "timeout": 60, "tests": [ { diff --git a/examples/kubernetes-ingress/tests.json b/examples/kubernetes-ingress/tests.json index 5049e3bee..caa4f01dc 100644 --- a/examples/kubernetes-ingress/tests.json +++ b/examples/kubernetes-ingress/tests.json @@ -2,7 +2,7 @@ "name": "kubernetes-ingress", "kinds": ["kubernetes"], "timeout": 60, - "delay": 60, + "delay": 120, "tests": [ { "type": "string", diff --git a/examples/kubernetes-tls/tests.json b/examples/kubernetes-tls/tests.json index 031368a2a..00c2c9aa6 100644 --- a/examples/kubernetes-tls/tests.json +++ b/examples/kubernetes-tls/tests.json @@ -1,26 +1,29 @@ { - "name": "kubernetes-ingress", + "name": "kubernetes-tls", "kinds": ["kubernetes"], "timeout": 60, - "delay": 60, + "delay": 300, "tests": [ { "type": "string", "url": "https://app1.example.com", "string": "hello", - "tls": "app1.example.com,app2.example.com" + "tls": "app1.example.com,app2.example.com", + "tls_edit": false }, { "type": "string", "url": "https://app2.example.com", "string": "hello", - "tls": "app1.example.com,app2.example.com" + "tls": "app1.example.com,app2.example.com", + "tls_edit": false }, { "type": "string", "url": "https://app3.example.com", "string": "hello", - "tls": "app3.example.com" + "tls": "app3.example.com", + "tls_edit": false } ] } diff --git a/examples/swarm-configs/tests.json b/examples/swarm-configs/tests.json index 53c474dae..da9979f12 100644 --- a/examples/swarm-configs/tests.json +++ b/examples/swarm-configs/tests.json @@ -1,7 +1,8 @@ { "name": "swarm-configs", "kinds": ["swarm"], - "timeout": 120, + "timeout": 60, + "delay": 120, "tests": [ { "type": "string", diff --git a/misc/integrations/k8s.mariadb.yml b/misc/integrations/k8s.mariadb.yml index 565a576f8..ba0aedd33 100644 --- a/misc/integrations/k8s.mariadb.yml +++ b/misc/integrations/k8s.mariadb.yml @@ -1,3 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: pvc-bunkerweb +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -247,14 +258,3 @@ spec: protocol: TCP port: 6379 targetPort: 6379 ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: pvc-bunkerweb -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 5Gi diff --git a/src/autoconf/DockerController.py b/src/autoconf/DockerController.py index 4529e176d..0c5e5aa56 100644 --- a/src/autoconf/DockerController.py +++ b/src/autoconf/DockerController.py @@ -100,10 +100,15 @@ def apply_config(self) -> bool: first=not self._loaded, ) + def __process_event(self, event): + return "Actor" in event and "Attributes" in event["Actor"] and ("bunkerweb.INSTANCE" in event["Actor"]["Attributes"] or "bunkerweb.SERVER_NAME" in event["Actor"]["Attributes"]) + def process_events(self): self._set_autoconf_load_db() - for _ in self.__client.events(decode=True, filters={"type": "container"}): + for event in self.__client.events(decode=True, filters={"type": "container"}): try: + if not self.__process_event(event): + continue self._update_settings() self._instances = self.get_instances() self._services = self.get_services() diff --git a/src/autoconf/Dockerfile b/src/autoconf/Dockerfile index 65330ff4b..a15fe88bc 100644 --- a/src/autoconf/Dockerfile +++ b/src/autoconf/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12.1-alpine3.18@sha256:af0d8da43677e3000ebdf4045508d891a87e7bd2d3ec87bc6e40403be97291b8 AS builder +FROM python:3.12.1-alpine3.18@sha256:fb759579d60cfe1f70b110a27be95aaa7cf758d2fa21cf54fffb71c2ba3f8034 AS builder # Copy python requirements COPY src/deps/requirements.txt /tmp/requirements-deps.txt @@ -34,7 +34,7 @@ COPY src/common/helpers helpers COPY src/common/settings.json settings.json COPY src/common/utils utils -FROM python:3.12.1-alpine3.18@sha256:af0d8da43677e3000ebdf4045508d891a87e7bd2d3ec87bc6e40403be97291b8 +FROM python:3.12.1-alpine3.18@sha256:fb759579d60cfe1f70b110a27be95aaa7cf758d2fa21cf54fffb71c2ba3f8034 # Set default umask to prevent huge recursive chmod increasing the final image size RUN umask 027 @@ -64,7 +64,7 @@ RUN apk add --no-cache bash && \ chmod 750 cli/main.py helpers/*.sh /usr/bin/bwcli autoconf/main.py deps/python/bin/* # Fix CVEs -# There are no CVEs to fix in this image +RUN apk add --no-cache "libcrypto3>=3.1.4-r3" "libssl3>=3.1.4-r3" "sqlite-libs>=3.41.2-r3" VOLUME /data /etc/nginx diff --git a/src/autoconf/IngressController.py b/src/autoconf/IngressController.py index 2e9db4a77..c49f5039c 100644 --- a/src/autoconf/IngressController.py +++ b/src/autoconf/IngressController.py @@ -146,15 +146,16 @@ def _to_services(self, controller_service) -> List[dict]: for host in tls.hosts: for service in services: if host in service["SERVER_NAME"].split(" "): - secret_tls = self.__corev1.list_secret_for_all_namespaces( + secrets_tls = self.__corev1.list_secret_for_all_namespaces( watch=False, field_selector=f"metadata.name={tls.secret_name},metadata.namespace={namespace}", ).items - if not secret_tls: + if len(secrets_tls) == 0: self._logger.warning( f"Ignoring tls setting for {host} : secret {tls.secret_name} not found.", ) break + secret_tls = secrets_tls[0] if not secret_tls.data: self._logger.warning( f"Ignoring tls setting for {host} : secret {tls.secret_name} contains no data.", @@ -227,6 +228,22 @@ def get_configs(self) -> dict: configs[config_type][f"{config_site}{config_name}"] = config_data return configs + def __process_event(self, event): + obj = event["object"] + metadata = obj.metadata if obj else None + annotations = metadata.annotations if metadata else None + if not obj: + return False + if obj.kind == "Pod": + return annotations and "bunkerweb.io/INSTANCE" in annotations + if obj.kind == "Ingress": + return True + if obj.kind == "ConfigMap": + return annotations and "bunkerweb.io/CONFIG_TYPE" in annotations + if obj.kind == "Service": + return True + return False + def __watch(self, watch_type): w = watch.Watch() what = None @@ -245,9 +262,13 @@ def __watch(self, watch_type): locked = False error = False try: - for _ in w.stream(what): + for event in w.stream(what): self.__internal_lock.acquire() locked = True + if not self.__process_event(event): + self.__internal_lock.release() + locked = False + continue self._update_settings() self._instances = self.get_instances() self._services = self.get_services() diff --git a/src/autoconf/SwarmController.py b/src/autoconf/SwarmController.py index d0976620b..9ec837f7e 100644 --- a/src/autoconf/SwarmController.py +++ b/src/autoconf/SwarmController.py @@ -16,14 +16,20 @@ def __init__(self, docker_host): super().__init__("swarm") self.__client = DockerClient(base_url=docker_host) self.__internal_lock = Lock() + self.__swarm_instances = [] + self.__swarm_services = [] + self.__swarm_configs = [] def _get_controller_instances(self) -> List[Service]: + self.__swarm_instances = [] return self.__client.services.list(filters={"label": "bunkerweb.INSTANCE"}) def _get_controller_services(self) -> List[Service]: + self.__swarm_services = [] return self.__client.services.list(filters={"label": "bunkerweb.SERVER_NAME"}) def _to_instances(self, controller_instance) -> List[dict]: + self.__swarm_instances.append(controller_instance.id) instances = [] instance_env = {} for env in controller_instance.attrs["Spec"]["TaskTemplate"]["ContainerSpec"]["Env"]: @@ -46,6 +52,7 @@ def _to_instances(self, controller_instance) -> List[dict]: return instances def _to_services(self, controller_service) -> List[dict]: + self.__swarm_services.append(controller_service.id) service = {} for variable, value in controller_service.attrs["Spec"]["Labels"].items(): if not variable.startswith("bunkerweb."): @@ -80,6 +87,7 @@ def _get_static_services(self) -> List[dict]: return services def get_configs(self) -> Dict[str, Dict[str, Any]]: + self.__swarm_configs = [] configs = {} for config_type in self._supported_config_types: configs[config_type] = {} @@ -103,6 +111,7 @@ def get_configs(self) -> Dict[str, Dict[str, Any]]: continue config_site = f"{config.attrs['Spec']['Labels']['bunkerweb.CONFIG_SITE']}/" configs[config_type][f"{config_site}{config_name}"] = b64decode(config.attrs["Spec"]["Data"]) + self.__swarm_configs.append(config.id) return configs def apply_config(self) -> bool: @@ -113,14 +122,40 @@ def apply_config(self) -> bool: first=not self._loaded, ) + def __process_event(self, event): + if "Actor" not in event or "ID" not in event["Actor"] or "Type" not in event: + return False + if event["Type"] not in ("service", "config"): + return False + if event["Type"] == "service": + if event["Actor"]["ID"] in self.__swarm_instances or event["Actor"]["ID"] in self.__swarm_services: + return True + try: + labels = self.__client.services.get(event["Actor"]["ID"]).attrs["Spec"]["Labels"] + return "bunkerweb.INSTANCE" in labels or "bunkerweb.SERVER_NAME" in labels + except: + return False + if event["Type"] == "config": + if event["Actor"]["ID"] in self.__swarm_configs: + return True + try: + return "bunkerweb.CONFIG_TYPE" in self.__client.configs.get(event["Actor"]["ID"]).attrs["Spec"]["Labels"] + except: + return False + return False + def __event(self, event_type): while True: locked = False error = False try: - for _ in self.__client.events(decode=True, filters={"type": event_type}): + for event in self.__client.events(decode=True, filters={"type": event_type}): self.__internal_lock.acquire() locked = True + if not self.__process_event(event): + self.__internal_lock.release() + locked = False + continue try: self._update_settings() self._instances = self.get_instances() @@ -137,6 +172,7 @@ def __event(self, event_type): self._logger.info( "Successfully deployed new configuration 🚀", ) + self._set_autoconf_load_db() except: self._logger.error(f"Exception while processing Swarm event ({event_type}) :\n{format_exc()}") self.__internal_lock.release() diff --git a/src/bw/Dockerfile b/src/bw/Dockerfile index 78fc5a6ea..adaa9451b 100644 --- a/src/bw/Dockerfile +++ b/src/bw/Dockerfile @@ -78,7 +78,7 @@ RUN apk add --no-cache pcre bash python3 yajl && \ ln -s /proc/1/fd/1 /var/log/bunkerweb/access.log # Fix CVEs -RUN apk add --no-cache "libwebp>=1.2.4-r3" "curl>=8.3.0-r0" "libcurl>=8.3.0-r0" "nghttp2-libs>=1.51.0-r2" "libcrypto3>=3.0.12-r0" "libssl3>=3.0.12-r0" "libx11>=1.8.7-r0" +RUN apk add --no-cache "libwebp>=1.2.4-r3" "curl>=8.3.0-r0" "libcurl>=8.3.0-r0" "nghttp2-libs>=1.51.0-r2" "libx11>=1.8.7-r0" "libssl3>=3.0.12-r1" "libcrypto3>=3.0.12-r1" EXPOSE 8080/tcp 8443/tcp diff --git a/src/bw/lua/bunkerweb/api.lua b/src/bw/lua/bunkerweb/api.lua index 9c2f9a2a8..abbcf4b2e 100644 --- a/src/bw/lua/bunkerweb/api.lua +++ b/src/bw/lua/bunkerweb/api.lua @@ -33,7 +33,6 @@ local get_body_data = ngx_req.get_body_data local get_body_file = ngx_req.get_body_file local decode = cjson.decode local encode = cjson.encode -local floor = math.floor local match = string.match local require_plugin = helpers.require_plugin local new_plugin = helpers.new_plugin @@ -216,7 +215,26 @@ api.global.POST["^/ban$"] = function(self) if not ok then return self:response(HTTP_INTERNAL_SERVER_ERROR, "error", "can't decode JSON : " .. ip) end - datastore:set("bans_ip_" .. ip["ip"], "manual", ip["exp"]) + local ban = { + ip = "", + exp = 86400, + reason = "manual", + } + ban.ip = ip["ip"] + if ip["exp"] then + ban.exp = ip["exp"] + end + if ip["reason"] then + ban.reason = ip["reason"] + end + datastore:set( + "bans_ip_" .. ban["ip"], + encode({ + reason = ban["reason"], + date = os.time(), + }), + ban["exp"] + ) return self:response(HTTP_OK, "success", "ip " .. ip["ip"] .. " banned") end @@ -224,12 +242,12 @@ api.global.GET["^/bans$"] = function(self) local data = {} for _, k in ipairs(datastore:keys()) do if k:find("^bans_ip_") then - local reason, err = datastore:get(k) + local result, err = datastore:get(k) if err then return self:response( HTTP_INTERNAL_SERVER_ERROR, "error", - "can't access " .. k .. " from datastore : " .. reason + "can't access " .. k .. " from datastore : " .. result ) end local ok, ttl = datastore:ttl(k) @@ -240,7 +258,9 @@ api.global.GET["^/bans$"] = function(self) "can't access ttl " .. k .. " from datastore : " .. ttl ) end - local ban = { ip = k:sub(9, #k), reason = reason, exp = floor(ttl) } + local ban_data = decode(result) + local ban = + { ip = k:sub(9, #k), reason = ban_data["reason"], date = ban_data["date"], exp = math.floor(ttl) } table.insert(data, ban) end end diff --git a/src/bw/lua/bunkerweb/clusterstore.lua b/src/bw/lua/bunkerweb/clusterstore.lua index ec7545afe..794f31938 100644 --- a/src/bw/lua/bunkerweb/clusterstore.lua +++ b/src/bw/lua/bunkerweb/clusterstore.lua @@ -2,7 +2,6 @@ local ngx = ngx local class = require "middleclass" local clogger = require "bunkerweb.logger" local rc = require "resty.redis.connector" -local rs = require("resty.redis.sentinel") local utils = require "bunkerweb.utils" local clusterstore = class("clusterstore") @@ -12,10 +11,10 @@ local logger = clogger:new("CLUSTERSTORE") local get_variable = utils.get_variable local is_cosocket_available = utils.is_cosocket_available local ERR = ngx.ERR +local WARN = ngx.WARN local INFO = ngx.INFO local tonumber = tonumber local tostring = tostring -local random = math.random function clusterstore:initialize(pool) -- Get variables @@ -25,6 +24,7 @@ function clusterstore:initialize(pool) ["REDIS_PORT"] = "", ["REDIS_DATABASE"] = "", ["REDIS_SSL"] = "", + ["REDIS_SSL_VERIFY"] = "", ["REDIS_TIMEOUT"] = "", ["REDIS_KEEPALIVE_IDLE"] = "", ["REDIS_KEEPALIVE_POOL"] = "", @@ -57,6 +57,7 @@ function clusterstore:initialize(pool) keepalive_poolsize = tonumber(self.variables["REDIS_KEEPALIVE_POOL"]), connection_options = { ssl = self.variables["REDIS_SSL"] == "yes", + ssl_verify = self.variables["REDIS_SSL_VERIFY"] == "yes", }, host = self.variables["REDIS_HOST"], port = tonumber(self.variables["REDIS_PORT"]), @@ -71,7 +72,6 @@ function clusterstore:initialize(pool) } self.pool = pool == nil or pool if self.pool then - options.connection_options.pool = "bw-redis" options.connection_options.pool_size = tonumber(self.variables["REDIS_KEEPALIVE_POOL"]) end if self.variables["REDIS_SENTINEL_HOSTS"] ~= "" then @@ -82,7 +82,14 @@ function clusterstore:initialize(pool) else sport = tonumber(sport) end - table.insert(options.sentinel, { host = shost, port = sport }) + local data = { host = shost, port = sport } + if options.sentinel_username ~= "" then + data.username = options.sentinel_username + end + if options.sentinel_password ~= "" then + data.password = options.sentinel_password + end + table.insert(options.sentinels, data) end end self.options = options @@ -107,33 +114,37 @@ function clusterstore:connect(readonly) self:close() end -- Connect to sentinels if needed - local redis_client, err - if #self.options.sentinels > 0 then - local redis_sentinel - redis_sentinel, err = self.redis_connector:connect() - if not redis_sentinel then - return false, "error while connecting to sentinels : " .. err - end - if readonly then - local redis_clients, _ = rs.get_slaves(redis_sentinel, self.options.master_name) - if redis_clients then - redis_client = redis_clients[random(#redis_clients)] - else - redis_client = nil + local redis_client, err, previous_errors + if #self.options.sentinels > 0 and readonly then + redis_client, err, previous_errors = self.redis_connector:connect({ role = "slave" }) + if not redis_client then + if previous_errors then + err = err .. " ( previous errors : " + for _, e in ipairs(previous_errors) do + err = err .. e .. ", " + end + err = err:sub(1, -3) .. " )" end - else - redis_client, err = rs.get_master(redis_sentinel, self.options.master_name) + logger:log(WARN, "error while getting redis slave client : " .. err .. ", fallback to master") + redis_client, err, previous_errors = self.redis_connector:connect() end - -- Classic connection else - redis_client, err = self.redis_connector:connect() + redis_client, err, previous_errors = self.redis_connector:connect() end self.redis_client = redis_client if not self.redis_client then + if previous_errors then + err = err .. " ( previous errors : " + for _, e in ipairs(previous_errors) do + err = err .. e .. ", " + end + err = err:sub(1, -3) .. " )" + end return false, "error while getting redis client : " .. err end -- Everything went well - local times, err = self.redis_client:get_reused_times() + local times + times, err = self.redis_client:get_reused_times() if times == nil then self:close() return false, "error while getting reused times : " .. err diff --git a/src/bw/lua/bunkerweb/datastore.lua b/src/bw/lua/bunkerweb/datastore.lua index c720d453a..72f86054f 100644 --- a/src/bw/lua/bunkerweb/datastore.lua +++ b/src/bw/lua/bunkerweb/datastore.lua @@ -15,8 +15,10 @@ if not lru then logger:log(ERR, "failed to instantiate LRU cache : " .. err_lru) end -function datastore:initialize() - if subsystem == "http" then +function datastore:initialize(dict) + if dict then + self.dict = dict + elseif subsystem == "http" then self.dict = shared.datastore else self.dict = shared.datastore_stream @@ -112,4 +114,32 @@ function datastore:flush_lru() lru:flush_all() end +function datastore:safe_rpush(key, value) + local length, err = self.dict:rpush(key, value) + if not length and err == "no memory" then + local i = 0 + while i < 5 do + local val + val, err = self.dict:lpop(key) + if not val then + return val, err + end + length, err = self.dict:rpush(key, value) + if not length and err ~= "no memory" then + return length, err + end + i = i + 1 + end + end + return length, err +end + +function datastore:lpop(key) + return self.dict:lpop(key) +end + +function datastore:llen(key) + return self.dict:llen(key) +end + return datastore diff --git a/src/bw/lua/bunkerweb/helpers.lua b/src/bw/lua/bunkerweb/helpers.lua index f417f2e04..3c41fa82f 100644 --- a/src/bw/lua/bunkerweb/helpers.lua +++ b/src/bw/lua/bunkerweb/helpers.lua @@ -185,6 +185,7 @@ helpers.fill_ctx = function(no_ref) end data.remote_addr = var.remote_addr data.server_name = var.server_name + data.local_time = var.local_time if data.kind == "http" then data.uri = var.uri data.request_uri = var.request_uri diff --git a/src/bw/lua/bunkerweb/utils.lua b/src/bw/lua/bunkerweb/utils.lua index 9b381c680..8b200ee59 100644 --- a/src/bw/lua/bunkerweb/utils.lua +++ b/src/bw/lua/bunkerweb/utils.lua @@ -29,7 +29,6 @@ local decode = cjson.decode local char = string.char local random = math.random local session_start = session.start -local session_open = session.open local tonumber = tonumber local utils = {} @@ -569,86 +568,86 @@ utils.get_deny_status = function() return 444 end -utils.check_session = function(ctx) - local _session, _, exists, _ = session_start({ audience = "metadata" }) +utils.get_session = function(ctx) + -- Return session from ctx if already there + if ctx.bw.sessions_session then + return ctx.bw.sessions_session + end + -- Open/create and do an optional refresh + local err, exists, refreshed + session, err, exists, refreshed = session_start() + if not session then + return nil, err + end + if err then + logger:log(WARN, "can't open session : " .. err) + end + local checks = { + ["IP"] = ctx.bw.remote_addr, + ["USER_AGENT"] = ctx.bw.http_user_agent or "", + } if exists then - for _, check in ipairs(ctx.bw.sessions_checks) do - local key = check[1] - local value = check[2] - if _session:get(key) ~= value then - _session:clear_request_cookie() - local ok, err = _session:destroy() - if not ok then - return false, "session:destroy() error : " .. err + logger:log(INFO, "opening an existing session") + if refreshed then + logger:log(INFO, "existing session refreshed") + end + -- Get metadata + local metadata = session:get("metadata") + if metadata then + -- Check if session passes the checks + for check, value in pairs(checks) do + local check_value + check_value, err = utils.get_variable("SESSIONS_CHECK_" .. check, false, nil) + if not check_value then + logger:log(ERR, "error while getting variable SESSIONS_CHECK_" .. check .. " : " .. err) + elseif check_value == "yes" and value ~= metadata[check] then + logger:log(WARN, "session check failed : " .. check .. "!=" .. metadata[check]) + local ok + ok, err = session:destroy() + if not ok then + return nil, err + end + return utils.get_session(ctx) end - logger:log(WARN, "session check " .. key .. " failed, destroying session") - return utils.check_session(ctx) end end else - for _, check in ipairs(ctx.bw.sessions_checks) do - _session:set(check[1], check[2]) - end - local ok, err = _session:save() - if not ok then - _session:close() - return false, "session:save() error : " .. err - end + logger:log(INFO, "creating a new session") + session:set("metadata", checks) + ctx.bw.sessions_updated = true end - ctx.bw.sessions_is_checked = true - return true, exists + ctx.bw.sessions_session = session + return session end -utils.get_session = function(audience, ctx) - -- Check session - if not ctx.bw.sessions_is_checked then - local ok, err = utils.check_session(ctx) - if not ok then - return false, "error while checking session, " .. err +utils.save_session = function(ctx) + if ctx.bw.sessions_session then + if ctx.bw.sessions_updated then + local ok, err = ctx.bw.sessions_session:save() + if not err then + err = "session saved" + end + return ok, err + else + return true, "session not updated" end + else + return true, "no session" end - -- Open session with specific audience - local _session, err, _ = session_open({ audience = audience }) - if err then - logger:log(INFO, "session:open() error : " .. err) - end - return _session -end - --- luacheck: ignore 214 -utils.get_session_data = function(_session, site, ctx) - local site_only = site == nil or site - local data = _session:get_data() - if site_only then - return data[ctx.bw.server_name] or {} - end - return data -end - --- luacheck: ignore 214 -utils.set_session_data = function(_session, data, site, ctx) - local site_only = site == nil or site - if site_only then - local all_data = _session:get_data() - all_data[ctx.bw.server_name] = data - _session:set_data(all_data) - return _session:save() - end - _session:set_data(data) - return _session:save() end utils.is_banned = function(ip) -- Check on local datastore - local reason, err = datastore:get("bans_ip_" .. ip) - if not reason and err ~= "not found" then - return nil, "datastore:get() error : " .. reason - elseif reason and err ~= "not found" then + local result, err = datastore:get("bans_ip_" .. ip) + if not result and err ~= "not found" then + return nil, "datastore:get() error : " .. result + elseif result and err ~= "not found" then local ok, ttl = datastore:ttl("bans_ip_" .. ip) + local ban_data = decode(result) if not ok then - return true, reason, -1 + return true, ban_data, -1 end - return true, reason, ttl + return true, ban_data, ttl end -- Redis case local use_redis, err = utils.get_variable("USE_REDIS", false) @@ -703,7 +702,11 @@ end utils.add_ban = function(ip, reason, ttl) -- Set on local datastore - local ok, err = datastore:set("bans_ip_" .. ip, reason, ttl) + local ban_data = encode({ + reason = reason, + date = os.time(), + }) + local ok, err = datastore:set("bans_ip_" .. ip, ban_data, ttl) if not ok then return false, "datastore:set() error : " .. err end @@ -721,7 +724,7 @@ utils.add_ban = function(ip, reason, ttl) return false, "can't connect to redis server : " .. err end -- SET call - ok, err = clusterstore:call("set", "bans_ip_" .. ip, reason, "EX", ttl) + ok, err = clusterstore:call("set", "bans_ip_" .. ip, ban_data, "EX", ttl) if not ok then clusterstore:close() return false, "redis SET failed : " .. err diff --git a/src/common/cli/CLI.py b/src/common/cli/CLI.py index b8b6cf754..1dfb13832 100644 --- a/src/common/cli/CLI.py +++ b/src/common/cli/CLI.py @@ -1,12 +1,15 @@ #!/usr/bin/env python3 +from datetime import datetime +from json import dumps, loads +from time import time from dotenv import dotenv_values from os import getenv, sep from os.path import join from pathlib import Path -from redis import StrictRedis +from redis import StrictRedis, Sentinel from sys import path as sys_path -from typing import Optional, Tuple +from typing import Any, Optional, Tuple for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in (("utils",), ("db",))]: @@ -19,10 +22,19 @@ def format_remaining_time(seconds): - days, seconds = divmod(seconds, 86400) - hours, seconds = divmod(seconds, 3600) + years, seconds = divmod(seconds, 60 * 60 * 24 * 365) + months, seconds = divmod(seconds, 60 * 60 * 24 * 30) + while months >= 12: + years += 1 + months -= 12 + days, seconds = divmod(seconds, 60 * 60 * 24) + hours, seconds = divmod(seconds, 60 * 60) minutes, seconds = divmod(seconds, 60) time_parts = [] + if years > 0: + time_parts.append(f"{int(years)} year{'' if years == 1 else 's'}") + if months > 0: + time_parts.append(f"{int(months)} month{'' if months == 1 else 's'}") if days > 0: time_parts.append(f"{int(days)} day{'' if days == 1 else 's'}") if hours > 0: @@ -49,6 +61,8 @@ def __init__(self): if Path(sep, "usr", "share", "bunkerweb", "db").exists(): from Database import Database # type: ignore + self.__logger.info("Getting variables from database") + db = Database(self.__logger, sqlalchemy_string=self.__get_variable("DATABASE_URI", None)) self.__variables = db.get_config() @@ -58,6 +72,7 @@ def __init__(self): self.__use_redis = self.__get_variable("USE_REDIS", "no") == "yes" self.__redis = None if self.__use_redis: + self.__logger.info("Fetching redis configuration") redis_host = self.__get_variable("REDIS_HOST") if redis_host: redis_port = self.__get_variable("REDIS_PORT", "6379") @@ -89,16 +104,71 @@ def __init__(self): redis_keepalive_pool = "10" redis_keepalive_pool = int(redis_keepalive_pool) - self.__redis = StrictRedis( - host=redis_host, - port=redis_port, - db=redis_db, - socket_timeout=redis_timeout, - socket_connect_timeout=redis_timeout, - socket_keepalive=True, - max_connections=redis_keepalive_pool, - ssl=self.__get_variable("REDIS_SSL", "no") == "yes", - ) + self.__logger.info("Redis configuration is valid") + + redis_ssl = self.__get_variable("REDIS_SSL", "no") == "yes" + username = self.__get_variable("REDIS_USERNAME", None) or None + password = self.__get_variable("REDIS_PASSWORD", None) or None + sentinel_hosts = self.__get_variable("REDIS_SENTINEL_HOSTS", []) + + if isinstance(sentinel_hosts, str): + sentinel_hosts = [host.split(":") if ":" in host else host for host in sentinel_hosts.split(" ") if host] + + if sentinel_hosts: + sentinel_username = self.__get_variable("REDIS_SENTINEL_USERNAME", None) or None + sentinel_password = self.__get_variable("REDIS_SENTINEL_PASSWORD", None) or None + sentinel_master = self.__get_variable("REDIS_SENTINEL_MASTER", "") + + self.__logger.info( + f"Connecting to redis sentinel cluster with the following parameters:\n{sentinel_hosts=}\n{sentinel_username=}\n{sentinel_password=}\n{sentinel_master=}\n{redis_timeout=}\nmax_connections={redis_keepalive_pool}\n{redis_ssl=}" + ) + sentinel = Sentinel( + sentinel_hosts, + username=sentinel_username, + password=sentinel_password, + ssl=redis_ssl, + socket_timeout=redis_timeout, + socket_connect_timeout=redis_timeout, + socket_keepalive=True, + max_connections=redis_keepalive_pool, + ) + try: + sentinel.discover_master(sentinel_master) + except Exception as e: + self.__logger.error(f"Failed to connect to redis sentinel cluster: {e}, disabling redis") + self.__use_redis = False + + if self.__use_redis: + self.__logger.info(f"Connected to redis sentinel cluster, getting master with the following parameters:\n{sentinel_master=}\n{redis_db=}\n{username=}\n{password=}") + self.__redis = sentinel.master_for( + sentinel_master, + db=redis_db, + username=username, + password=password, + ) + else: + self.__logger.info(f"Connecting to redis with the following parameters:\n{redis_host=}\n{redis_port=}\n{redis_db=}\n{username=}\n{password=}\n{redis_timeout=}\nmax_connections={redis_keepalive_pool}\n{redis_ssl=}") + self.__redis = StrictRedis( + host=redis_host, + port=redis_port, + db=redis_db, + username=username, + password=password, + socket_timeout=redis_timeout, + socket_connect_timeout=redis_timeout, + socket_keepalive=True, + max_connections=redis_keepalive_pool, + ssl=redis_ssl, + ) + + try: + if self.__use_redis: + assert self.__redis, "Redis connection is None" + self.__redis.ping() + except Exception as e: + self.__logger.error(f"Failed to connect to redis: {e}, disabling redis") + self.__use_redis = False + self.__logger.info("Connected to redis") else: self.__logger.error("USE_REDIS is set to yes but REDIS_HOST is not set, disabling redis") self.__use_redis = False @@ -116,7 +186,7 @@ def __init__(self): super().__init__() self.auto_setup(self.__integration) - def __get_variable(self, variable: str, default: Optional[str] = None) -> Optional[str]: + def __get_variable(self, variable: str, default: Optional[Any] = None) -> Optional[str]: return getenv(variable, self.__variables.get(variable, default)) def __detect_integration(self) -> str: @@ -148,14 +218,15 @@ def unban(self, ip: str) -> Tuple[bool, str]: return True, f"IP {ip} has been unbanned" return False, "error" - def ban(self, ip: str, exp: float) -> Tuple[bool, str]: + def ban(self, ip: str, exp: float, reason: str) -> Tuple[bool, str]: if self.__redis: - ok = self.__redis.set(f"bans_ip_{ip}", "manual", ex=exp) + ok = self.__redis.set(f"bans_ip_{ip}", dumps({"reason": reason, "date": time()})) if not ok: self.__logger.error(f"Failed to ban {ip} in redis") + self.__redis.expire(f"bans_ip_{ip}", int(exp)) - if self.send_to_apis("POST", "/ban", data={"ip": ip, "exp": exp}): - return (True, f"IP {ip} has been banned for {format_remaining_time(exp)}") + if self.send_to_apis("POST", "/ban", data={"ip": ip, "exp": exp, "reason": reason}): + return (True, f"IP {ip} has been banned for {format_remaining_time(exp)} with reason {reason}") return False, "error" def bans(self) -> Tuple[bool, str]: @@ -172,8 +243,13 @@ def bans(self) -> Tuple[bool, str]: servers["redis"] = [] for key in self.__redis.scan_iter("bans_ip_*"): ip = key.decode("utf-8").replace("bans_ip_", "") + data = self.__redis.get(key) + if not data: + continue exp = self.__redis.ttl(key) - servers["redis"].append({"ip": ip, "exp": exp, "reason": "manual"}) + servers["redis"].append({"ip": ip, "exp": exp} | loads(data)) + + servers = {k: sorted(v, key=lambda x: x["date"]) for k, v in servers.items()} cli_str = "" for server, bans in servers.items(): @@ -182,7 +258,7 @@ def bans(self) -> Tuple[bool, str]: cli_str += "No ban found\n" for ban in bans: - cli_str += f"- {ban['ip']} for {format_remaining_time(ban['exp'])} : {ban.get('reason', 'no reason given')}\n" + cli_str += f"- {ban['ip']} ; banned the {datetime.fromtimestamp(ban['date']).strftime('%d-%m-%Y at %H:%M:%S')} for {format_remaining_time(ban['exp'])} remaining with reason \"{ban.get('reason', 'no reason given')}\"\n" cli_str += "\n" return True, cli_str diff --git a/src/common/cli/main.py b/src/common/cli/main.py index 92473b7c7..486e8f5d2 100644 --- a/src/common/cli/main.py +++ b/src/common/cli/main.py @@ -40,6 +40,12 @@ help=f"banning time in seconds (default : {ban_time})", default=ban_time, ) + parser_ban.add_argument( + "-reason", + type=str, + help="reason for ban (default : manual)", + default="manual", + ) # Bans subparser parser_bans = subparsers.add_parser("bans", help="list current bans") @@ -55,7 +61,7 @@ if args.command == "unban": ret, err = cli.unban(args.ip) elif args.command == "ban": - ret, err = cli.ban(args.ip, args.exp) + ret, err = cli.ban(args.ip, args.exp, args.reason) elif args.command == "bans": ret, err = cli.bans() diff --git a/src/common/confs/api.conf b/src/common/confs/api.conf index 61efdd377..f804ffa46 100644 --- a/src/common/confs/api.conf +++ b/src/common/confs/api.conf @@ -13,6 +13,11 @@ server { # default mime type is JSON default_type 'application/json'; + # variables + set $reason ''; + set $reason_data ''; + set $ctx_ref ''; + # check IP and do the API call access_by_lua_block { -- Instantiate objects and import required modules diff --git a/src/common/confs/default-server-http.conf b/src/common/confs/default-server-http.conf index 84bc51da4..0afea7423 100644 --- a/src/common/confs/default-server-http.conf +++ b/src/common/confs/default-server-http.conf @@ -1,7 +1,9 @@ server { - # reason variable + # variables set $reason ''; + set $reason_data ''; + set $ctx_ref ''; server_name _; @@ -99,6 +101,8 @@ server { local ok, ret = call_plugin(plugin_obj, "log_default") if not ok then logger:log(ERR, ret) + elseif not ret.ret then + logger:log(ERR, plugin_id .. ":log_default() call failed : " .. ret.msg) else logger:log(INFO, plugin_id .. ":log_default() call successful : " .. ret.msg) end diff --git a/src/common/confs/server-http/access-lua.conf b/src/common/confs/server-http/access-lua.conf index a707fcebb..1de1793df 100644 --- a/src/common/confs/server-http/access-lua.conf +++ b/src/common/confs/server-http/access-lua.conf @@ -22,6 +22,7 @@ access_by_lua_block { local is_banned = utils.is_banned local set_reason = utils.set_reason local get_deny_status = utils.get_deny_status + local save_session = utils.save_session local tostring = tostring -- Don't process internal requests @@ -120,6 +121,14 @@ access_by_lua_block { end logger:log(INFO, "called access() methods of plugins") + -- Save session + ok, err = save_session(ctx) + if ok then + logger:log(INFO, err) + else + logger:log(ERR, err) + end + -- Save ctx save_ctx(ctx) diff --git a/src/common/confs/server-http/ssl-certificate-lua.conf b/src/common/confs/server-http/ssl-certificate-lua.conf index b4c7ebfef..a8cd9b7f6 100644 --- a/src/common/confs/server-http/ssl-certificate-lua.conf +++ b/src/common/confs/server-http/ssl-certificate-lua.conf @@ -85,6 +85,7 @@ ssl_certificate_by_lua_block { if not ok then logger:log(ERR, "error while setting private key : " .. err) else + logger:log(INFO, "certificate set by " .. plugin_id) return true end end diff --git a/src/common/core/antibot/antibot.lua b/src/common/core/antibot/antibot.lua index 14cab8f9e..d7ef474f2 100644 --- a/src/common/core/antibot/antibot.lua +++ b/src/common/core/antibot/antibot.lua @@ -12,11 +12,10 @@ local ngx = ngx local subsystem = ngx.config.subsystem local HTTP_INTERNAL_SERVER_ERROR = ngx.HTTP_INTERNAL_SERVER_ERROR local OK = ngx.OK +local INFO = ngx.INFO local tonumber = tonumber local tostring = tostring local get_session = utils.get_session -local get_session_data = utils.get_session_data -local set_session_data = utils.set_session_data local get_deny_status = utils.get_deny_status local rand = utils.rand local now = ngx.now @@ -47,36 +46,25 @@ function antibot:header() end -- Check if antibot uri if self.ctx.bw.uri ~= self.variables["ANTIBOT_URI"] then - return self:ret(true, "Not antibot uri") + return self:ret(true, "not antibot uri") end -- Get session data - local session, err = get_session("antibot", self.ctx) - if not session then - return self:ret(false, "can't get session : " .. err, HTTP_INTERNAL_SERVER_ERROR) + self.session_data = self.ctx.bw.antibot_session_data + if not self.session_data then + return self:ret(false, "can't get session data", HTTP_INTERNAL_SERVER_ERROR) end - self.session = session - self.session_data = get_session_data(self.session, true, self.ctx) - -- Check if session is valid - self:check_session() -- Don't go further if client resolved the challenge if self.session_data.resolved then - if self.ctx.bw.uri == self.variables["ANTIBOT_URI"] then - return self:ret(true, "client already resolved the challenge", nil, self.session_data.original_uri) - end - return self:ret(true, "client already resolved the challenge") - end - - if self.ctx.bw.uri ~= self.variables["ANTIBOT_URI"] then - return self:ret(true, "not antibot uri") + return self:ret(true, "client already resolved the challenge", nil, self.session_data.original_uri) end + -- Override headers local header = "Content-Security-Policy" if self.variables["CONTENT_SECURITY_POLICY_REPORT_ONLY"] == "yes" then header = header .. "-Report-Only" end - if self.session_data.type == "recaptcha" then ngx.header[header] = "default-src 'none'; form-action 'self'; script-src 'strict-dynamic' 'nonce-" .. self.session_data.nonce_script @@ -108,7 +96,7 @@ function antibot:header() .. self.session_data.nonce_style .. "'; font-src 'self' data:; base-uri 'self';" end - return self:ret(true, "Successfully overridden CSP header") + return self:ret(true, "successfully overridden CSP header") end function antibot:access() @@ -118,14 +106,17 @@ function antibot:access() end -- Get session data - local session, err = get_session("antibot", self.ctx) + local session, err = get_session(self.ctx) if not session then - return self:ret(false, "can't get session : " .. err, HTTP_INTERNAL_SERVER_ERROR) + return self:ret(false, "can't get session : " .. err) end self.session = session - self.session_data = get_session_data(self.session, true, self.ctx) + self.session_data = session:get("antibot") or {} + self.ctx.bw.antibot_session_data = self.session_data + -- Check if session is valid - self:check_session() + local msg = self:check_session() + self.logger:log(INFO, "check_session returned : " .. msg) -- Don't go further if client resolved the challenge if self.session_data.resolved then @@ -137,10 +128,6 @@ function antibot:access() -- Prepare challenge if needed self:prepare_challenge() - local ok, err = self:set_session_data() - if not ok then - return self:ret(false, "can't save session : " .. err, HTTP_INTERNAL_SERVER_ERROR) - end -- Redirect to challenge page if self.ctx.bw.uri ~= self.variables["ANTIBOT_URI"] then @@ -162,10 +149,6 @@ function antibot:access() if self.ctx.bw.request_method == "POST" then -- luacheck: ignore 421 local ok, err, redirect = self:check_challenge() - local set_ok, set_err = self:set_session_data() - if not set_ok then - return self:ret(false, "can't save session : " .. set_err, HTTP_INTERNAL_SERVER_ERROR) - end if ok == nil then return self:ret(false, "check challenge error : " .. err, HTTP_INTERNAL_SERVER_ERROR) elseif not ok then @@ -175,10 +158,6 @@ function antibot:access() return self:ret(true, "check challenge redirect : " .. redirect, nil, redirect) end self:prepare_challenge() - ok, err = self:set_session_data() - if not ok then - return self:ret(false, "can't save session : " .. err, HTTP_INTERNAL_SERVER_ERROR) - end self.ctx.bw.antibot_display_content = true return self:ret(true, "displaying challenge to client", OK) end @@ -202,12 +181,10 @@ function antibot:content() end -- Get session data - local session, err = get_session("antibot", self.ctx) - if not session then - return self:ret(false, "can't get session : " .. err, HTTP_INTERNAL_SERVER_ERROR) + self.session_data = self.ctx.bw.antibot_session_data + if not self.session_data then + return self:ret(false, "missing session data", HTTP_INTERNAL_SERVER_ERROR) end - self.session = session - self.session_data = get_session_data(self.session, true, self.ctx) -- Direct access without session if not self.session_data.prepared then @@ -229,42 +206,36 @@ function antibot:check_session() -- Not resolved and not prepared if not time_resolve and not time_valid then self.session_data = {} - self.session_updated = true - return + self:set_session_data() + return "not prepared" end -- Check if still valid - local time = ngx.now() + local time = now() local resolved = self.session_data.resolved if resolved and (time_valid > time or time - time_valid > tonumber(self.variables["ANTIBOT_TIME_VALID"])) then self.session_data = {} - self.session_updated = true - return + self:set_session_data() + return "need new resolve" end -- Check if new prepare is needed if not resolved and (time_resolve > time or time - time_resolve > tonumber(self.variables["ANTIBOT_TIME_RESOLVE"])) then self.session_data = {} - self.session_updated = true - return + self:set_session_data() + return "need new prepare" end + return "valid" end function antibot:set_session_data() - if self.session_updated then - local ok, err = set_session_data(self.session, self.session_data, true, self.ctx) - if not ok then - return false, err - end - self.session_updated = false - return true, "updated" - end - return true, "no update" + self.session:set("antibot", self.session_data) + self.ctx.bw.antibot_session_data = self.session_data + self.ctx.bw.sessions_updated = true end function antibot:prepare_challenge() if not self.session_data.prepared then - self.session_updated = true self.session_data.prepared = true self.session_data.time_resolve = ngx.now() self.session_data.type = self.variables["USE_ANTIBOT"] @@ -283,6 +254,7 @@ function antibot:prepare_challenge() elseif self.session_data.type == "captcha" then self.session_data.captcha = rand(6, true) end + self:set_session_data() end end @@ -363,6 +335,7 @@ function antibot:check_challenge() end self.session_data.resolved = true self.session_data.time_valid = now() + self:set_session_data() return true, "resolved", self.session_data.original_uri end @@ -374,10 +347,11 @@ function antibot:check_challenge() return nil, "missing challenge arg", nil end if self.session_data.captcha ~= args["captcha"] then - return false, "wrong value", nil + return false, "wrong value, expected " .. self.session_data.captcha, nil end self.session_data.resolved = true self.session_data.time_valid = now() + self:set_session_data() return true, "resolved", self.session_data.original_uri end @@ -417,6 +391,7 @@ function antibot:check_challenge() end self.session_data.resolved = true self.session_data.time_valid = now() + self:set_session_data() return true, "resolved", self.session_data.original_uri end @@ -456,6 +431,7 @@ function antibot:check_challenge() end self.session_data.resolved = true self.session_data.time_valid = now() + self:set_session_data() return true, "resolved", self.session_data.original_uri end @@ -495,6 +471,7 @@ function antibot:check_challenge() end self.session_data.resolved = true self.session_data.time_valid = now() + self:set_session_data() return true, "resolved", self.session_data.original_uri end diff --git a/src/common/core/antibot/files/captcha.html b/src/common/core/antibot/files/captcha.html index c43054dc6..dd49f3949 100644 --- a/src/common/core/antibot/files/captcha.html +++ b/src/common/core/antibot/files/captcha.html @@ -250,6 +250,7 @@ class="mt-3 px-2 text-gray-800 h-8 w-full max-w-[300px] rounded-lg outline-secondary" type="text" name="captcha" + required /> + + `; + let cleanHTML = DOMPurify.sanitize(item); + this.listEl.insertAdjacentHTML("beforeend", cleanHTML); + this.setDatepicker(this.itemCount); + } + + setDatepicker(id) { + // instantiate datepicker + const dateOptions = { + locale: "en", + dateFormat: "m/d/Y H:i:S", + defaultDate: false, + enableTime: true, + enableSeconds: true, + time_24hr: true, + minuteIncrement: 1, + onChange(selectedDates, dateStr, instance) { + const inpEl = document.querySelector(`input#ban-end-${id}`); + + // Get date to timestamp + const pickStamp = +Date.parse(new Date(dateStr).toString()); + const nowStamp = +Date.now(); + + // Case pick is before current date + if (pickStamp < nowStamp) { + inpEl.setAttribute("data-timestamp", Date.now()); + return instance.setDate(nowStamp); + } + + // Case right value + // Set timestamp in seconds in the related input + const convertToS = +pickStamp.toString().substring(0, 10); + + inpEl.setAttribute("data-timestamp", convertToS); + }, + }; + + flatpickr(`input#ban-end-${id}`, dateOptions); + } +} + +const setDropdown = new Dropdown(); +const setFilter = new Filter(); +const setUnban = new Unban(); +const setModal = new AddBanModal(); diff --git a/src/ui/static/js/jobs.js b/src/ui/static/js/jobs.js index 019509fcb..4c73e5f2e 100644 --- a/src/ui/static/js/jobs.js +++ b/src/ui/static/js/jobs.js @@ -47,7 +47,9 @@ class Dropdown { //close dropdown and change style this.hideDropdown(btnSetting); - if (!e.target.closest("button").hasAttribute(`data-${prefix}-file`)) { + if ( + !e.target.closest("button").hasAttribute(`data-${this.prefix}-file`) + ) { this.changeDropBtnStyle(btnSetting, btn); } //show / hide filter diff --git a/src/ui/static/js/plugins.js b/src/ui/static/js/plugins.js index 71b5bea71..1e17858ba 100644 --- a/src/ui/static/js/plugins.js +++ b/src/ui/static/js/plugins.js @@ -111,13 +111,7 @@ class Dropdown { const btnEls = dropdownEl.querySelectorAll("button"); btnEls.forEach((btn) => { - btn.classList.remove( - "dark:bg-primary", - "bg-primary", - "bg-primary", - "text-gray-300", - "text-gray-300", - ); + btn.classList.remove("dark:bg-primary", "bg-primary", "text-gray-300"); btn.classList.add("bg-white", "dark:bg-slate-700", "text-gray-700"); }); //highlight clicked btn diff --git a/src/ui/static/js/reports.js b/src/ui/static/js/reports.js new file mode 100644 index 000000000..aab6932b9 --- /dev/null +++ b/src/ui/static/js/reports.js @@ -0,0 +1,372 @@ +class Filter { + constructor(prefix = "reports") { + this.prefix = prefix; + this.container = document.querySelector(`[data-${this.prefix}-filter]`); + this.keyInp = document.querySelector("input#keyword"); + this.methodValue = "all"; + this.statusValue = "all"; + this.reasonValue = "all"; + this.countryValue = "all"; + this.initHandler(); + } + + initHandler() { + //METHOD HANDLER + this.container.addEventListener("click", (e) => { + try { + if ( + e.target + .closest("button") + .getAttribute(`data-${this.prefix}-setting-select-dropdown-btn`) === + "method" + ) { + setTimeout(() => { + const value = document + .querySelector( + `[data-${this.prefix}-setting-select-text="method"]`, + ) + .textContent.trim(); + + this.methodValue = value; + //run filter + this.filter(); + }, 10); + } + } catch (err) {} + }); + //COUNTRY HANDLER + this.container.addEventListener("click", (e) => { + try { + if ( + e.target + .closest("button") + .getAttribute(`data-${this.prefix}-setting-select-dropdown-btn`) === + "country" + ) { + setTimeout(() => { + const value = document + .querySelector( + `[data-${this.prefix}-setting-select-text="country"]`, + ) + .textContent.trim(); + + this.countryValue = value; + //run filter + this.filter(); + }, 10); + } + } catch (err) {} + }); + //STATUS HANDLER + this.container.addEventListener("click", (e) => { + try { + if ( + e.target + .closest("button") + .getAttribute(`data-${this.prefix}-setting-select-dropdown-btn`) === + "status" + ) { + setTimeout(() => { + const value = document + .querySelector( + `[data-${this.prefix}-setting-select-text="status"]`, + ) + .textContent.trim(); + + this.statusValue = value; + //run filter + this.filter(); + }, 10); + } + } catch (err) {} + }); + // REASON HANDLER + +this.container.addEventListener("click", (e) => { + try { + if ( + e.target + .closest("button") + .getAttribute(`data-${this.prefix}-setting-select-dropdown-btn`) === + "reason" + ) { + setTimeout(() => { + const value = document + .querySelector( + `[data-${this.prefix}-setting-select-text="reason"]`, + ) + .textContent.trim(); + + this.reasonValue = value; + //run filter + this.filter(); + }, 10); + } + } catch (err) {} + }); + //KEYWORD HANDLER + this.keyInp.addEventListener("input", (e) => { + this.filter(); + }); + } + + filter() { + const requests = document.querySelector( + `[data-${this.prefix}-list]`, + ).children; + if (requests.length === 0) return; + //reset + for (let i = 0; i < requests.length; i++) { + const el = requests[i]; + el.classList.remove("hidden"); + } + //filter type + this.setFilterMethod(requests); + this.setFilterKeyword(requests); + this.setFilterStatus(requests); + this.setFilterReason(requests); + this.setFilterCountry(requests); + } + + setFilterMethod(requests) { + if (this.methodValue === "all") return; + for (let i = 0; i < requests.length; i++) { + const el = requests[i]; + const type = this.getElAttribut(el, "method"); + if (type !== this.methodValue) el.classList.add("hidden"); + } + } + + setFilterMethod(requests) { + if (this.countryValue === "all") return; + for (let i = 0; i < requests.length; i++) { + const el = requests[i]; + const type = this.getElAttribut(el, "country"); + if (type !== this.countryValue) el.classList.add("hidden"); + } + } + + setFilterKeyword(requests) { + const keyword = this.keyInp.value.trim().toLowerCase(); + if (!keyword) return; + for (let i = 0; i < requests.length; i++) { + const el = requests[i]; + + const url = this.getElAttribut(el, "url"); + const date = this.getElAttribut(el, "date"); + const ip = this.getElAttribut(el, "ip"); + const data = this.getElAttribut(el, "data"); + + if ( + !url.includes(keyword) && + !date.includes(keyword) && + !ip.includes(keyword) && + !data.includes(keyword) + ) + el.classList.add("hidden"); + } + } + + setFilterStatus(requests) { + if (this.statusValue === "all") return; + for (let i = 0; i < requests.length; i++) { + const el = requests[i]; + const type = this.getElAttribut(el, "status"); + if (type !== this.statusValue) el.classList.add("hidden"); + } + } + + setFilterReason(requests) { + if (this.reasonValue === "all") return; + for (let i = 0; i < requests.length; i++) { + const el = requests[i]; + const type = this.getElAttribut(el, "reason"); + if (type !== this.reasonValue) el.classList.add("hidden"); + } + } + + getElAttribut(el, attr) { + return el + .querySelector(`[data-${this.prefix}-${attr}]`) + .getAttribute(`data-${this.prefix}-${attr}`) + .trim(); + } +} + +class Dropdown { + constructor(prefix = "reports") { + this.prefix = prefix; + this.container = document.querySelector("main"); + this.lastDrop = ""; + this.initDropdown(); + } + + initDropdown() { + this.container.addEventListener("click", (e) => { + //SELECT BTN LOGIC + try { + if ( + e.target + .closest("button") + .hasAttribute(`data-${this.prefix}-setting-select`) && + !e.target.closest("button").hasAttribute(`disabled`) + ) { + const btnName = e.target + .closest("button") + .getAttribute(`data-${this.prefix}-setting-select`); + if (this.lastDrop !== btnName) { + this.lastDrop = btnName; + this.closeAllDrop(); + } + + this.toggleSelectBtn(e); + } + } catch (err) {} + //SELECT DROPDOWN BTN LOGIC + try { + if ( + e.target + .closest("button") + .hasAttribute(`data-${this.prefix}-setting-select-dropdown-btn`) + ) { + const btn = e.target.closest("button"); + const btnValue = btn.getAttribute("value"); + const btnSetting = btn.getAttribute( + `data-${this.prefix}-setting-select-dropdown-btn`, + ); + //stop if same value to avoid new fetching + const isSameVal = this.isSameValue(btnSetting, btnValue); + if (isSameVal) return this.hideDropdown(btnSetting); + //else, add new value to custom + this.setSelectNewValue(btnSetting, btnValue); + //close dropdown and change style + this.hideDropdown(btnSetting); + + if ( + !e.target.closest("button").hasAttribute(`data-${this.prefix}-file`) + ) { + this.changeDropBtnStyle(btnSetting, btn); + } + //show / hide filter + if (btnSetting === "instances") { + this.hideFilterOnLocal(btn.getAttribute("data-_type")); + } + } + } catch (err) {} + }); + } + + closeAllDrop() { + const drops = document.querySelectorAll( + `[data-${this.prefix}-setting-select-dropdown]`, + ); + drops.forEach((drop) => { + drop.classList.add("hidden"); + drop.classList.remove("flex"); + document + .querySelector( + `svg[data-${this.prefix}-setting-select="${drop.getAttribute( + `data-${this.prefix}-setting-select-dropdown`, + )}"]`, + ) + .classList.remove("rotate-180"); + }); + } + + isSameValue(btnSetting, value) { + const selectCustom = document.querySelector( + `[data-${this.prefix}-setting-select-text="${btnSetting}"]`, + ); + const currVal = selectCustom.textContent; + return currVal === value ? true : false; + } + + setSelectNewValue(btnSetting, value) { + const selectCustom = document.querySelector( + `[data-${this.prefix}-setting-select="${btnSetting}"]`, + ); + selectCustom.querySelector( + `[data-${this.prefix}-setting-select-text]`, + ).textContent = value; + } + + hideDropdown(btnSetting) { + //hide dropdown + const dropdownEl = document.querySelector( + `[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`, + ); + dropdownEl.classList.add("hidden"); + dropdownEl.classList.remove("flex"); + //svg effect + const dropdownChevron = document.querySelector( + `svg[data-${this.prefix}-setting-select="${btnSetting}"]`, + ); + dropdownChevron.classList.remove("rotate-180"); + } + + changeDropBtnStyle(btnSetting, selectedBtn) { + const dropdownEl = document.querySelector( + `[data-${this.prefix}-setting-select-dropdown="${btnSetting}"]`, + ); + //reset dropdown btns + const btnEls = dropdownEl.querySelectorAll("button"); + + btnEls.forEach((btn) => { + btn.classList.remove( + "bg-primary", + "dark:bg-primary", + "text-gray-300", + "text-gray-300", + ); + btn.classList.add("bg-white", "dark:bg-slate-700", "text-gray-700"); + }); + //highlight clicked btn + selectedBtn.classList.remove( + "bg-white", + "dark:bg-slate-700", + "text-gray-700", + ); + selectedBtn.classList.add("dark:bg-primary", "bg-primary", "text-gray-300"); + } + + toggleSelectBtn(e) { + const attribute = e.target + .closest("button") + .getAttribute(`data-${this.prefix}-setting-select`); + //toggle dropdown + const dropdownEl = document.querySelector( + `[data-${this.prefix}-setting-select-dropdown="${attribute}"]`, + ); + const dropdownChevron = document.querySelector( + `svg[data-${this.prefix}-setting-select="${attribute}"]`, + ); + dropdownEl.classList.toggle("hidden"); + dropdownEl.classList.toggle("flex"); + dropdownChevron.classList.toggle("rotate-180"); + } + + //hide date filter on local + hideFilterOnLocal(type) { + if (type === "local") { + this.hideInp(`input#from-date`); + this.hideInp(`input#to-date`); + } + + if (type !== "local") { + this.showInp(`input#from-date`); + this.showInp(`input#to-date`); + } + } + + showInp(selector) { + document.querySelector(selector).closest("div").classList.add("flex"); + document.querySelector(selector).closest("div").classList.remove("hidden"); + } + + hideInp(selector) { + document.querySelector(selector).closest("div").classList.add("hidden"); + document.querySelector(selector).closest("div").classList.remove("flex"); + } +} + +const setDropdown = new Dropdown(); +const setFilter = new Filter(); diff --git a/src/ui/static/js/services.js b/src/ui/static/js/services.js index 3e938f1ab..89fe7e974 100644 --- a/src/ui/static/js/services.js +++ b/src/ui/static/js/services.js @@ -83,6 +83,37 @@ class ServiceModal { this.openModal(); } } catch (err) {} + // clone action + try { + if ( + e.target.closest("button").getAttribute("data-services-action") === + "clone" + ) { + //set form info and right form + const [action, serviceName] = this.getActionAndServName(e.target); + this.setForm(action, serviceName, serviceName, this.formNewEdit); + //set default value with method default + //get service data and parse it + //multiple type logic is launch at same time on relate class + const servicesSettings = e.target + .closest("[data-services-service]") + .querySelector("[data-services-settings]") + .getAttribute("data-value"); + const obj = JSON.parse(servicesSettings); + this.updateModalData(obj, true); + // server name is unset + const inpServName = document.querySelector("input#SERVER_NAME"); + inpServName.getAttribute("value", ""); + inpServName.removeAttribute("disabled", ""); + inpServName.value = ""; + // clone is UI creation, so no setting should be disabled + + //show modal + this.resetFilterInp(); + this.changeSubmitBtn("CREATE", "valid-btn"); + this.openModal(); //server name is unset + } + } catch (err) {} //new action try { if ( @@ -219,10 +250,11 @@ class ServiceModal { setForm(action, serviceName, oldServName, formEl) { this.modalTitle.textContent = `${action} ${serviceName}`; - formEl.setAttribute("id", `form-${action}-${serviceName}`); + const operation = action === "clone" ? "new" : action; + formEl.setAttribute("id", `form-${operation}-${serviceName}`); const opeInp = formEl.querySelector(`input[name="operation"]`); - opeInp.setAttribute("value", action); - opeInp.value = action; + opeInp.setAttribute("value", operation); + opeInp.value = operation; if (action === "edit" || action === "new") { this.showNewEditForm(); @@ -231,6 +263,13 @@ class ServiceModal { oldNameInp.value = oldServName; } + if (action === "clone") { + this.showNewEditForm(); + const oldNameInp = formEl.querySelector(`input[name="OLD_SERVER_NAME"]`); + oldNameInp.setAttribute("value", ""); + oldNameInp.value = ""; + } + if (action === "delete") { this.showDeleteForm(); formEl.querySelector(`[data-services-modal-text]`).textContent = @@ -286,7 +325,7 @@ class ServiceModal { this.modalTabsHeader.classList.remove("hidden"); } - updateModalData(settings) { + updateModalData(settings, forceEnabled = false) { //use this to select inputEl and change value for (const [key, data] of Object.entries(settings)) { //change format to match id @@ -350,14 +389,14 @@ class ServiceModal { inp.setAttribute("data-method", method); } - //check disabled/enabled after setting values and methods - this.setDisabledServ(inp, method, global); + if (!forceEnabled) this.setDisabledState(inp, method, global); + if (forceEnabled) inp.removeAttribute("disabled"); }); } catch (err) {} } } - setDisabledServ(inp, method, global) { + setDisabledState(inp, method, global) { if (global) return inp.removeAttribute("disabled"); if (method === "ui" || method === "default") { diff --git a/src/ui/static/js/totp.js b/src/ui/static/js/totp.js index 8dd1fd9d5..17b600c87 100644 --- a/src/ui/static/js/totp.js +++ b/src/ui/static/js/totp.js @@ -12,8 +12,8 @@ class BackLogin { "href", window.location.href.replace( `/${this.currEndpoint}`, - `/${this.backEndpoint}` - ) + `/${this.backEndpoint}`, + ), ); }); }); diff --git a/src/ui/styles.css b/src/ui/styles.css index e67dc6c81..9bacc7433 100644 --- a/src/ui/styles.css +++ b/src/ui/styles.css @@ -25,23 +25,23 @@ } .close-btn { - @apply dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-red-500 border border-red-500 uppercase align-middle transition-all rounded-lg cursor-pointer dark:bg-gray-200 dark:hover:brightness-75 bg-white hover:bg-white/80 focus:bg-white/80 leading-normal ease-in tracking-tight-rem shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md; + @apply dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-red-500 border border-red-500 uppercase align-middle transition-all rounded-lg cursor-pointer dark:bg-gray-200 dark:hover:brightness-75 bg-white hover:bg-white/80 focus:bg-white/80 leading-normal ease-in tracking-tight-rem shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md disabled:cursor-not-allowed dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-700 dark:disabled:border-gray-700/0 disabled:hover:translate-y-0 disabled:hover:bg-gray-400 disabled:hover:border-gray-400/0 dark:disabled:hover:translate-y-0 dark:disabled:hover:bg-gray-700 dark:disabled:hover:border-gray-700/0; } .valid-btn { - @apply tracking-wide dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-green-500 hover:bg-green-500/80 focus:bg-green-500/80 leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md; + @apply tracking-wide dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-green-500 hover:bg-green-500/80 focus:bg-green-500/80 leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md disabled:cursor-not-allowed dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-700 dark:disabled:border-gray-700/0 disabled:hover:translate-y-0 disabled:hover:bg-gray-400 disabled:hover:border-gray-400/0 dark:disabled:hover:translate-y-0 dark:disabled:hover:bg-gray-700 dark:disabled:hover:border-gray-700/0; } .delete-btn { - @apply tracking-wide dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-red-500 hover:bg-red-500/80 focus:bg-red-500/80 leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md; + @apply tracking-wide dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-red-500 hover:bg-red-500/80 focus:bg-red-500/80 leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md disabled:cursor-not-allowed dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-700 dark:disabled:border-gray-700/0 disabled:hover:translate-y-0 disabled:hover:bg-gray-400 disabled:hover:border-gray-400/0 dark:disabled:hover:translate-y-0 dark:disabled:hover:bg-gray-700 dark:disabled:hover:border-gray-700/0; } .edit-btn { - @apply tracking-wide dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-yellow-500 hover:bg-yellow-500/80 focus:bg-yellow-500/80 leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md; + @apply tracking-wide dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-yellow-500 hover:bg-yellow-500/80 focus:bg-yellow-500/80 leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md disabled:cursor-not-allowed dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-700 dark:disabled:border-gray-700/0 disabled:hover:translate-y-0 disabled:hover:bg-gray-400 disabled:hover:border-gray-400/0 dark:disabled:hover:translate-y-0 dark:disabled:hover:bg-gray-700 dark:disabled:hover:border-gray-700/0; } .info-btn { - @apply tracking-wide dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-sky-500 hover:bg-sky-500/80 focus:bg-sky-500/80 leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md; + @apply tracking-wide dark:brightness-90 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-sky-500 hover:bg-sky-500/80 focus:bg-sky-500/80 leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md disabled:cursor-not-allowed dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-700 dark:disabled:border-gray-700/0 disabled:hover:translate-y-0 disabled:hover:bg-gray-400 disabled:hover:border-gray-400/0 dark:disabled:hover:translate-y-0 dark:disabled:hover:bg-gray-700 dark:disabled:hover:border-gray-700/0; } /*----------------------------------------------*/ diff --git a/src/ui/templates/banner.html b/src/ui/templates/banner.html index 67e8d2e78..250ef4ac2 100644 --- a/src/ui/templates/banner.html +++ b/src/ui/templates/banner.html @@ -42,7 +42,7 @@ class="dark:brightness-125 font-medium underline text-gray-100 hover:no-underline" href="https://demo.bunkerweb.io/link/?utm_campaign=self&utm_source=ui" > - demo wep app ! + demo web app !

diff --git a/src/ui/templates/bans.html b/src/ui/templates/bans.html new file mode 100644 index 000000000..084bf0e46 --- /dev/null +++ b/src/ui/templates/bans.html @@ -0,0 +1,364 @@ +{% extends "base.html" %} {% block content %} {% set current_endpoint = +url_for(request.endpoint)[1:].split("/")[-1].strip() %} + +{% set reasons = [] %} +{% set terms = [] %} + +{% for ban in bans %} + {% if ban["reason"] not in reasons %} + {% set reasons = reasons.append(ban["reason"]) %} + {% endif %} + {% if ban["term"] not in terms %} + {% set terms = terms.append(ban["term"]) %} + {% endif %} +{% endfor %} + + +
+ +
+ + + +
+
INFO
+
+

+ BANS TOTAL +

+

+ {{bans|length}} +

+
+
+

+ TOP REASON +

+

+ {{top_reason}} +

+
+
+ + + +
+
FILTER
+
+ +
+
+ Search +
+ + +
+ + + +
+
+ Reason +
+ + + + + +
+ + + +
+
+ Range +
+ + + + + +
+ +
+
+ + +
+
+
BANS LIST
+
+ +
+
+ +
+ +

+ Select +

+

+ IP +

+

+ Reason +

+

+ Ban start +

+

+ Ban end +

+

+ Remain +

+ + + +
    + {% for ban in bans %} +
  • + +
    + + + + + + +
    +

    + {{ban['ip']}} +

    +

    + {{ban["reason"]}} +

    +

    + {{ban["ban_start"]}} +

    +

    + {{ban["ban_end"]}} +

    +

    + {{ban["remain"]}} +

    + +
  • + {% endfor %} +
+ +
+ +
+
+ +
+ + + + +
+
+ +{% include "bans_modal.html" %} + +{% endblock %} diff --git a/src/ui/templates/bans_modal.html b/src/ui/templates/bans_modal.html new file mode 100644 index 000000000..f4cd86e8d --- /dev/null +++ b/src/ui/templates/bans_modal.html @@ -0,0 +1,200 @@ + + + diff --git a/src/ui/templates/head.html b/src/ui/templates/head.html index da23effb1..4dd27eb73 100644 --- a/src/ui/templates/head.html +++ b/src/ui/templates/head.html @@ -45,5 +45,12 @@ {% elif current_endpoint == "account" %} + {% elif current_endpoint == "reports" %} + + {% elif current_endpoint == "bans" %} + + + + {% endif %} diff --git a/src/ui/templates/home.html b/src/ui/templates/home.html index 8982f0e46..73ff4f2f7 100644 --- a/src/ui/templates/home.html +++ b/src/ui/templates/home.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% block content %} - + + + {% endblock %} diff --git a/src/ui/templates/jobs.html b/src/ui/templates/jobs.html index 64030f728..f752cac29 100644 --- a/src/ui/templates/jobs.html +++ b/src/ui/templates/jobs.html @@ -2,7 +2,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
INFO
@@ -35,7 +35,7 @@
INFO
FILTER
@@ -197,215 +197,220 @@
FILTER
-
-
JOBS
- -
- -

- Name -

-

- Last run -

-

- Every -

-

- Reload -

-

- Success -

-

- Files -

- - -
    - {% for job_name, value in jobs.items() %} - -
  • +
    JOBS LIST
    +
+
+ +
+ +

-

- {{job_name}} -

-

- {{value['last_run']}} -

-

+

+ Last run +

+

+ Every +

+

+ Reload +

+

+ Success +

+

+ Files +

+ + +
    + {% for job_name, value in jobs.items() %} + +
  • - {{value["every"]}} -

    - {% if value["reload"] %} -

    - - - -

    + {{job_name}} +

    +

    + {{value['last_run']}} +

    +

    - - - -

    - {% endif %} {% if value["success"] %} -

    + {% if value["reload"] %} +

    - - - -

    - {% elif not value["success"] %} -

    + + +

    - class="ml-6 dark:text-gray-400 dark:opacity-80 text-sm col-span-1 m-0 my-1" - data-{{current_endpoint}}-success="false" - > - - - -

    - {% endif %} -
    + + +

    + {% endif %} {% if value["success"] %} +

    - {% if value['cache']%} - - - +

    + {% endif %} - - {%endif%} -
    -
  • - - {% endfor %} -
- + + + {% endfor %} + + +
+
-
{% endblock %} diff --git a/src/ui/templates/logs.html b/src/ui/templates/logs.html index 4fb5412fe..d2aa51288 100644 --- a/src/ui/templates/logs.html +++ b/src/ui/templates/logs.html @@ -67,6 +67,7 @@
SETTINGS
> From date +
SETTINGS pattern="(.*?)" required /> + + + +
@@ -85,6 +90,7 @@
SETTINGS
> To date (default today) +
SETTINGS pattern="(.*?)" required /> + + + +
@@ -104,6 +114,7 @@
SETTINGS
> Update delay (in seconds) + SETTINGS
FILTERS
@@ -279,31 +290,42 @@
FILTERS
-
-
LOGS
- -
- -

- Type -

-

- Description -

- - -
    - +
    +
    LOGS
    +
    + + +
    +
    + +
    + +

    + Type +

    +

    + Description +

    + + + +
      + +
      + +
      -
      {% endblock %} diff --git a/src/ui/templates/menu.html b/src/ui/templates/menu.html index e5c4a4bf0..a4973626b 100644 --- a/src/ui/templates/menu.html +++ b/src/ui/templates/menu.html @@ -133,20 +133,12 @@
      - - - + + + +
      Instances
    • +
      + + + + +
      + + Reporting + +
      +
    • + + +
    • + +
      + + + + + + +
      + + Bans + +
      +
    • + + + +
    • +
      LogsJobs
    • -
    • JobsLogs +
    • + diff --git a/src/ui/templates/plugins.html b/src/ui/templates/plugins.html index fc8de90a3..ea0197556 100644 --- a/src/ui/templates/plugins.html +++ b/src/ui/templates/plugins.html @@ -4,7 +4,7 @@
      INFO
      @@ -113,7 +113,7 @@
      UPLOAD / RELOAD
      FILTER
      diff --git a/src/ui/templates/reports.html b/src/ui/templates/reports.html new file mode 100644 index 000000000..7d2679d70 --- /dev/null +++ b/src/ui/templates/reports.html @@ -0,0 +1,483 @@ +{% extends "base.html" %} {% block content %} {% set current_endpoint = +url_for(request.endpoint)[1:].split("/")[-1].strip() %} + +{% set methods = [] %} +{% set codes = [] %} +{% set reasons = [] %} +{% set countries = [] %} + + +{% for report in reports %} + {% if report["method"] not in methods %} + {% set methods = methods.append(report["method"]) %} + {% endif %} + {% if report["status"] not in codes %} + {% set codes = codes.append(report["status"]) %} + {% endif %} + {% if report["reason"] not in reasons %} + {% set reasons = reasons.append(report["reason"]) %} + {% endif %} + {% if report["country"] not in countries %} + {% set countries = countries.append(report["country"]) %} + {% endif %} +{% endfor %} + + +
      +
      INFO
      +
      +

      + REPORTING TOTAL +

      +

      + {{reports|length}} +

      +
      +
      +

      + TOP REASON +

      +

      + {{top_reason}} +

      +
      +
      +

      + TOP STATUS CODE +

      +

      + {{top_code}} +

      +
      +
      + + + +
      +
      FILTER
      +
      + +
      +
      + Search +
      + + +
      + + + +
      +
      + Country +
      + + + + + +
      + + + +
      +
      + Method +
      + + + + + +
      + + + + +
      +
      + Status code +
      + + + + + +
      + + + +
      +
      + Reason +
      + + + + + +
      + +
      +
      + + +
      +
      +
      REPORTING
      +
      + +
      +
      + +

      + Date +

      +

      + IP +

      +

      + Country +

      +

      + Method +

      +

      + URL +

      +

      + Code +

      +

      + User agent +

      +

      + Reason +

      +

      + Data +

      + + +
        + {% for report in reports %} +
      • +

        + {{report['date']}} +

        +

        + {{report['ip']}} +

        +

        + {{report['country']}} +

        +

        + {{report["method"]}} +

        +

        + {{report['url']}} +

        +

        + {{report["status"]}} +

        +

        + {{report["user_agent"]}} +

        +

        + {{report["reason"]}} +

        +

        + {{report["data"]}} +

        +
      • + {% endfor %} +
      + +
      +
      +
      + +{% endblock %} diff --git a/src/ui/templates/services.html b/src/ui/templates/services.html index c91f0407a..3b85dbba9 100644 --- a/src/ui/templates/services.html +++ b/src/ui/templates/services.html @@ -7,15 +7,19 @@ data-services-action="new" data-services-name="service" type="button" - class="dark:bg-green-500/90 duration-300 dark:opacity-90 w-80 inline-block px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-green-500 hover:bg-green-500/80 focus:bg-green-500/80 leading-normal text-base ease-in tracking-tight-rem shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md" + class="dark:bg-green-500/90 duration-300 dark:opacity-90 w-80 flex justify-center items-center px-6 py-3 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer bg-green-500 hover:bg-green-500/80 focus:bg-green-500/80 leading-normal text-base ease-in tracking-tight-rem shadow-xs bg-150 bg-x-25 hover:-translate-y-px active:opacity-85 hover:shadow-md" > - New SERVICE + new service + + + +
      {% if services|length == 0 %}
      @@ -29,7 +33,7 @@ services_batched %} {% set id_server_name = service["SERVER_NAME"]['value'].replace(".", "-") %}
      @@ -355,6 +359,19 @@
      + + + + + +