diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1e371505c..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@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0 + 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@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0 + 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 85adacf3c..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 }} 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 ad70387e2..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 }} 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 5a5ab655c..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 }} diff --git a/.github/workflows/push-packagecloud.yml b/.github/workflows/push-packagecloud.yml index 216e3c726..2a2113256 100644 --- a/.github/workflows/push-packagecloud.yml +++ b/.github/workflows/push-packagecloud.yml @@ -42,7 +42,7 @@ 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 diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index c4be7af18..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@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0 + 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.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 115951f9e..f3729cea1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [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 diff --git a/README.md b/README.md index 714bbff8b..bb5462235 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- BunkerWeb logo + BunkerWeb logo

@@ -170,6 +170,10 @@ Another core component of BunkerWeb is the ModSecurity Web Application Firewall ## Database +

+ Database model +

+ State of the current configuration of BunkerWeb is stored in a backend database which contains the following data : - Settings defined for all the services diff --git a/docs/assets/img/bunkerweb_db.svg b/docs/assets/img/bunkerweb_db.svg index d7f898638..4ef50e01e 100644 --- a/docs/assets/img/bunkerweb_db.svg +++ b/docs/assets/img/bunkerweb_db.svg @@ -1 +1,12 @@ -1**11*1*1*1*1**1*11*bw_selectssetting_idvarchar[256]valuevarchar[256]bw_settingsidvarchar[256]namevarchar[256]plugin_idvarchar[64]contextcontexesdefaultvarchar[4096]helpvarchar[512]labelvarchar[256]regexvarchar[1024]typesettings_typesmultiplevarchar[128]bw_services_settingsservice_idvarchar[64]setting_idvarchar[256]valuevarchar[4096]suffixintmethodmethodsbw_servicesidvarchar[64]methodmethodsbw_global_valuessetting_idvarchar[256]valuevarchar[4096]suffixintmethodmethodsbw_pluginsidvarchar[64]namevarchar[128]descriptionvarchar[256]versionvarchar[32]streamvarchar[16]externalbooleanmethodmethodsdatalongblobchecksumvarchar[128]bw_jobsnamevarchar[128]plugin_idvarchar[64]file_namevarchar[256]everyschedulesreloadbooleansuccessbooleanlast_rundatetimebw_jobs_cacheidintjob_namevarchar[128]service_idvarchar[64]file_namevarchar[256]datalongbloblast_updatedatetimechecksumvarchar[128]bw_instanceshostnamevarchar[256]portintserver_namevarchar[256]bw_metadataidintis_initializedbooleanfirst_config_savedbooleanautoconf_loadedbooleanscheduler_first_startbooleancustom_configs_changedbooleanexternal_plugins_changedbooleanconfig_changedbooleanintegrationintegrationsversionvarcharbw_plugin_pagesidintplugin_idvarchar[64]template_filelongblobtemplate_checksumvarchar[128]actions_filelongblobactions_checksumvarchar[128]bw_custom_configsidintservice_idvarchar[64]typecustom_config_typesnamevarchar[256]datalongblobchecksumvarchar[128]methodmethods \ No newline at end of file +*11*1**1*11**11*1**1bw_instanceshostnamevarchar[256]portintserver_namevarchar[256]bw_servicesidvarchar[64]methodmethodsbw_global_valuessetting_idvarchar[256]valuevarchar[8192]suffixintmethodmethodsbw_plugin_pagesidintplugin_idvarchar[64]template_filelongblobtemplate_checksumvarchar[128]actions_filelongblobactions_checksumvarchar[128]bw_custom_configsidintservice_idvarchar[64]typecustom_config_typesnamevarchar[256]datalongblobchecksumvarchar[128]methodmethodsbw_services_settingsservice_idvarchar[64]setting_idvarchar[256]valuevarchar[8192]suffixintmethodmethodsbw_settingsidvarchar[256]namevarchar[256]plugin_idvarchar[64]contextcontexesdefaultvarchar[4096]helpvarchar[512]labelvarchar[256]regexvarchar[1024]typesettings_typesmultiplevarchar[128]bw_selectssetting_idvarchar[256]valuevarchar[256]bw_ui_usersidintusernamevarchar[256]passwordvarchar[60]is_two_factor_enabledbooleansecret_tokenvarchar[32]methodmethodsbw_jobs_cacheidintjob_namevarchar[128]service_idvarchar[64]file_namevarchar[256]datalongbloblast_updatedatetimechecksumvarchar[128]bw_jobsnamevarchar[128]plugin_idvarchar[64]file_namevarchar[256]everyschedulesreloadbooleansuccessbooleanlast_rundatetimebw_pluginsidvarchar[64]namevarchar[128]descriptionvarchar[256]versionvarchar[32]streamvarchar[16]externalbooleanmethodmethodsdatalongblobchecksumvarchar[128]bw_metadataidintis_initializedbooleanfirst_config_savedbooleanautoconf_loadedbooleanscheduler_first_startbooleancustom_configs_changedbooleanexternal_plugins_changedbooleanconfig_changedbooleaninstances_changedbooleanintegrationintegrationsversionvarchar \ No newline at end of file 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/quickstart-guide.md b/docs/quickstart-guide.md index f98cbbf29..973b0d755 100644 --- a/docs/quickstart-guide.md +++ b/docs/quickstart-guide.md @@ -1,10 +1,12 @@ # Quickstart guide !!! info "Prerequisites" + We assume that you're already familiar with the [core concepts](concepts.md) and you have followed the [integrations instructions](integrations.md) for your environment. !!! tip "Going further" - To demonstrate the use of BunkerWeb, we will deploy a dummy "Hello World" web application as an example. See the [examples folder](https://github.com/bunkerity/bunkerweb/tree/v1.5.5/examples) of the repository to get real-world examples. + + To demonstrate the use of BunkerWeb, we will deploy a dummy "Hello World" web application as an example. See the [examples folder](https://github.com/bunkerity/bunkerweb/tree/v1.5.5/examples) of the repository to get real-world examples. ## Protect HTTP applications @@ -1117,7 +1119,8 @@ REAL_IP_HEADER=proxy_protocol ## Protect UDP/TCP applications !!! warning "Feature is in beta" - This feature is not production-ready. Feel free to test it and report us any bug using [issues](https://github.com/bunkerity/bunkerweb/issues) in the GitHub repository. + + This feature is not production-ready. Feel free to test it and report us any bug using [issues](https://github.com/bunkerity/bunkerweb/issues) in the GitHub repository. BunkerWeb offers the capability to function as a **generic UDP/TCP reverse proxy**, allowing you to protect any network-based applications operating at least on layer 4 of the OSI model. Instead of utilizing the "classical" HTTP module, BunkerWeb leverages the [stream module](https://nginx.org/en/docs/stream/ngx_stream_core_module.html) of NGINX. @@ -2329,7 +2332,8 @@ BunkerWeb supports PHP using external or remote [PHP-FPM](https://www.php.net/ma ## IPv6 !!! warning "Feature is in beta" - This feature is not production-ready. Feel free to test it and report us any bug using [issues](https://github.com/bunkerity/bunkerweb/issues) in the GitHub repository. + + This feature is not production-ready. Feel free to test it and report us any bug using [issues](https://github.com/bunkerity/bunkerweb/issues) in the GitHub repository. By default, BunkerWeb will only listen on IPv4 addresses and won't use IPv6 for network communications. If you want to enable IPv6 support, you need to set `USE_IPV6=yes`. Please note that IPv6 configuration of your network and environment is out-of-the-scope of this documentation. 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 29bf34752..8c12ad156 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -225,67 +225,67 @@ markdown==3.5.2 \ # 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..f02949633 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 @@ -617,6 +636,7 @@ Review your final BunkerWeb UI URL and then click on the `Setup` button. Once th labels: app: bunkerweb-ui spec: + serviceAccountName: sa-bunkerweb containers: - name: bunkerweb-ui image: bunkerity/bunkerweb-ui:1.5.5 @@ -695,6 +715,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 +796,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. @@ -1452,6 +1476,7 @@ After a successful login/password combination, you will be prompted to enter you labels: app: bunkerweb-ui spec: + serviceAccountName: sa-bunkerweb containers: - name: bunkerweb-ui image: bunkerity/bunkerweb-ui:1.5.5 diff --git a/src/autoconf/Dockerfile b/src/autoconf/Dockerfile index c2151afd9..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 -RUN apk add --no-cache "libcrypto3>=3.1.4-r3" "libssl3>=3.1.4-r3" +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/SwarmController.py b/src/autoconf/SwarmController.py index dd415f364..9ec837f7e 100644 --- a/src/autoconf/SwarmController.py +++ b/src/autoconf/SwarmController.py @@ -125,7 +125,7 @@ class SwarmController(Controller): 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"]: + 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: 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..4b163ebca 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 = {} @@ -318,7 +317,7 @@ utils.get_reason = function(ctx) end local banned, _ = datastore:get("bans_ip_" .. ip) if banned then - return banned, {} + return decode(banned)["reason"], {} end -- unknown if ngx.status == utils.get_deny_status() then @@ -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["reason"], -1 end - return true, reason, ttl + return true, ban_data["reason"], ttl end -- Redis case local use_redis, err = utils.get_variable("USE_REDIS", false) @@ -695,7 +694,7 @@ utils.is_banned = function(ip) if not ok then return nil, "datastore:set() error : " .. err end - return true, data[1], data[2] + return true, decode(data[1])["reason"], data[2] end clusterstore:close() return false, "not banned" @@ -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 @@ from logger import setup_logger # type: ignore 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 @@ class CLI(ApiCaller): 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 @@ class CLI(ApiCaller): 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 @@ class CLI(ApiCaller): 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 @@ class CLI(ApiCaller): 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 @@ class CLI(ApiCaller): 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 @@ class CLI(ApiCaller): 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 @@ class CLI(ApiCaller): 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 @@ if __name__ == "__main__": 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 __name__ == "__main__": 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/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/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 @@ -46,37 +45,26 @@ function antibot:header() return self:ret(true, "antibot not activated") end -- Check if antibot uri - if self.ctx.bw.uri ~= self.variables["ANTIBOT_URI"] then - 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) - 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") end + -- Get session data + 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 + + -- Don't go further if client resolved the challenge + if self.session_data.resolved then + 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) { + const defaultDate = +(Date.now() + 3600000 * 24); + const inpEl = document.querySelector(`input#ban-end-${id}`); + inpEl.setAttribute("data-timestamp", defaultDate.toString().substring(0, 10)); + + // instantiate datepicker + const dateOptions = { + locale: "en", + dateFormat: "m/d/Y H:i:S", + defaultDate: defaultDate, + enableTime: true, + enableSeconds: true, + time_24hr: true, + minuteIncrement: 1, + onChange(selectedDates, dateStr, instance) { + // 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", defaultDate.toString().substring(0, 10)); + return instance.setDate(defaultDate); + } + + // 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/bans.html b/src/ui/templates/bans.html new file mode 100644 index 000000000..6824c1e90 --- /dev/null +++ b/src/ui/templates/bans.html @@ -0,0 +1,374 @@ +{% 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 %} + + +
+ +
+ + +
+ + + +
No bans found
+
+ + +
+
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..ee9a9c02c --- /dev/null +++ b/src/ui/templates/bans_modal.html @@ -0,0 +1,211 @@ + + + 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/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 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
FILTER
@@ -197,215 +197,220 @@ url_for(request.endpoint)[1:].split("/")[-1].strip() %}
-
-
JOBS
- -
- -

- Name -

-

- Last run -

-

- Every -

-

- Reload -

-

- Success -

-

- Files -

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

-

+

+ Last run +

+

+ Every +

+

+ Reload +

+

+ Success +

+

+ Files +

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

    -

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

    -

    - {{value["every"]}} -

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

    - - - -

    + {{job_name}} +

    +

    - - - -

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

    +

    - - - -

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

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

    - - - -

    - {% endif %} -
    - {% if value['cache']%} - - - - - - {%endif%} -
    -
  • - - {% endfor %} -
- + + + {% endfor %} + + +
+
-
{% endblock %} diff --git a/src/ui/templates/loading.html b/src/ui/templates/loading.html index 1bc3834c4..60db9fe5a 100644 --- a/src/ui/templates/loading.html +++ b/src/ui/templates/loading.html @@ -5,9 +5,8 @@ {{ message }}... 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 @@ url_for(request.endpoint)[1:].split("/")[-1].strip().replace('_', '-') %} > From date +
+ + + +
@@ -85,6 +90,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip().replace('_', '-') %} > To date (default today) +
+ + + +
@@ -104,6 +114,7 @@ url_for(request.endpoint)[1:].split("/")[-1].strip().replace('_', '-') %} > Update delay (in seconds) +
FILTERS
@@ -279,31 +290,42 @@ url_for(request.endpoint)[1:].split("/")[-1].strip().replace('_', '-') %}
-
-
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
    • - - - + + + +
      Logs + Reporting +
    • + +
    • + +
      + + + + + + +
      + + Bans + +
      +
    • + +
    • + +
    • + +
      + + + +
      + Logs + +
      +
    • + 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 @@ include "plugins_modal.html" %}
      INFO
      @@ -113,7 +113,7 @@ include "plugins_modal.html" %}
      FILTER
      diff --git a/src/ui/templates/plugins_modal.html b/src/ui/templates/plugins_modal.html index db19d7dc0..d0679294d 100644 --- a/src/ui/templates/plugins_modal.html +++ b/src/ui/templates/plugins_modal.html @@ -5,7 +5,7 @@ >

      - diff --git a/src/ui/templates/reports.html b/src/ui/templates/reports.html new file mode 100644 index 000000000..f5dfa2ed3 --- /dev/null +++ b/src/ui/templates/reports.html @@ -0,0 +1,493 @@ +{% 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 %} + +
      + + + +
      No reports found
      +
      + + +{% if reports|length != 0 %} +
      +
      INFO
      +
      +

      + REPORTING TOTAL +

      +

      + {{ total_reports }} +

      +
      +
      +

      + 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 %} +
      + +
      +
      +
      +{%endif%} + +{% 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 @@ + +