diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 7fc309e..d091db5 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -52,8 +52,6 @@ jobs: # override redis.conf ddev get julienloizelet/ddev-tools ddev get julienloizelet/ddev-crowdsec-php - ddev get julienloizelet/ddev-playwright - - name: Start DDEV run: ddev start @@ -86,7 +84,7 @@ jobs: run: | ddev exec /usr/bin/php ./${{env.EXTENSION_PATH}}/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./${{env.EXTENSION_PATH}}/tests/Unit - - name: Prepare PHP Integration and end-to-end tests + - name: Prepare PHP Integration tests run: | mkdir ${{ github.workspace }}/cfssl cp -r .ddev/okaeli-add-on/custom_files/crowdsec/cfssl/* ${{ github.workspace }}/cfssl @@ -101,15 +99,15 @@ jobs: - name: Run "IP verification with file_get_contents" test run: | - ddev exec BOUNCER_KEY=${{ env.BOUNCER_KEY }} AGENT_TLS_PATH=/var/www/html/cfssl LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./${{env.EXTENSION_PATH}}/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./${{env.EXTENSION_PATH}}/tests/Integration/IpVerificationTest.php + ddev exec BOUNCER_KEY=${{ env.BOUNCER_KEY }} AGENT_TLS_PATH=/var/www/html/cfssl LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./${{env.EXTENSION_PATH}}/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./${{env.EXTENSION_PATH}}/tests/Integration/AbstractBouncerTest.php - name: Run "IP verification with cURL" test run: | - ddev exec BOUNCER_KEY=${{ env.BOUNCER_KEY }} AGENT_TLS_PATH=/var/www/html/cfssl USE_CURL=1 LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./${{env.EXTENSION_PATH}}/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./${{env.EXTENSION_PATH}}/tests/Integration/IpVerificationTest.php + ddev exec BOUNCER_KEY=${{ env.BOUNCER_KEY }} AGENT_TLS_PATH=/var/www/html/cfssl USE_CURL=1 LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./${{env.EXTENSION_PATH}}/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./${{env.EXTENSION_PATH}}/tests/Integration/AbstractBouncerTest.php - name: Run "IP verification with TLS" test run: | - ddev exec AGENT_TLS_PATH=/var/www/html/cfssl BOUNCER_TLS_PATH=/var/www/html/cfssl LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./${{env.EXTENSION_PATH}}/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./${{env.EXTENSION_PATH}}/tests/Integration/IpVerificationTest.php + ddev exec AGENT_TLS_PATH=/var/www/html/cfssl BOUNCER_TLS_PATH=/var/www/html/cfssl LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./${{env.EXTENSION_PATH}}/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./${{env.EXTENSION_PATH}}/tests/Integration/AbstractBouncerTest.php - name: Run "Geolocation with file_get_contents" test run: | @@ -118,302 +116,3 @@ jobs: - name: Run "Geolocation with cURL" test run: | ddev exec BOUNCER_KEY=${{ env.BOUNCER_KEY }} AGENT_TLS_PATH=/var/www/html/cfssl USE_CURL=1 LAPI_URL=https://crowdsec:8080 /usr/bin/php ./${{env.EXTENSION_PATH}}/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./${{env.EXTENSION_PATH}}/tests/Integration/GeolocationTest.php - - - name: Prepare Standalone Bouncer end-to-end tests - run: | - cd ${{ github.workspace }}/.ddev - ddev nginx-config okaeli-add-on/native/custom_files/crowdsec/crowdsec-prepend-nginx-site.conf - cd ${{ github.workspace }} - cp ${{env.EXTENSION_PATH}}/tests/end-to-end/settings/base.php.dist crowdsec-lib-settings.php - sed -i -e 's/REPLACE_API_KEY/${{ env.BOUNCER_KEY }}/g' crowdsec-lib-settings.php - sed -i -e 's/REPLACE_PROXY_IP/${{ env.PROXY_IP }}/g' crowdsec-lib-settings.php - sed -i -e 's/REPLACE_FORCED_IP//g' crowdsec-lib-settings.php - sed -i -e 's/REPLACE_FORCED_FORWARDED_IP//g' crowdsec-lib-settings.php - mv crowdsec-lib-settings.php ${{env.EXTENSION_PATH}}/scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/__scripts__ - chmod +x test-init.sh - ./test-init.sh - chmod +x run-tests.sh - - - name: Verify auto_prepend_file directive - run: | - cd ${{ github.workspace }} - cp .ddev/okaeli-add-on/common/custom_files/phpinfo.php ${{env.EXTENSION_PATH}}/scripts/public/phpinfo.php - curl -v https://${{env.DDEV_PROJECT}}.ddev.site/${{env.EXTENSION_PATH}}/scripts/public/phpinfo.php - PREPENDVERIF=$(curl https://${{env.DDEV_PROJECT}}.ddev.site/${{env.EXTENSION_PATH}}/scripts/public/phpinfo.php | grep -o -E "auto_prepend_file=(.*)php(.*)" | sed 's/<\/tr>//g; s/<\/td>//g;' | tr '\n' '#') - if [[ $PREPENDVERIF == "auto_prepend_file=/var/www/html/${{env.EXTENSION_PATH}}/scripts/auto-prepend/bounce.php#auto_prepend_file=/var/www/html/my-code/crowdsec-bouncer-lib/scripts/auto-prepend/bounce.php#" ]] - then - echo "AUTO PREPEND FILE OK" - else - echo "AUTO PREPEND FILE KO" - echo $PREPENDVERIF - exit 1 - fi - - - name: Run "live mode with file_get_contents and without geolocation" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/1-live-mode.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "Display error with bad settings" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27cache_system\x27 => Constants::CACHE_SYSTEM_PHPFS/\x27cache_system\x27 => 1/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/6-display-error-on.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "No display error with bad settings" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27cache_system\x27 => Constants::CACHE_SYSTEM_PHPFS/\x27cache_system\x27 => 1/g' scripts/auto-prepend/settings.php - sed -i 's/\x27display_errors\x27 => true/\x27display_errors\x27 => false/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/5-display-error-off.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "No display error with error while bouncing" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27cache_system\x27 => 1/\x27cache_system\x27 => Constants::CACHE_SYSTEM_PHPFS/g' scripts/auto-prepend/settings.php - sed -i 's/\x27forced_test_ip\x27 => \x27\x27/\x27forced_test_ip\x27 => \x27bad-ip\x27/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/5-display-error-off.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "Display error with error while bouncing" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27display_errors\x27 => false/\x27display_errors\x27 => true/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/6-display-error-on.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "live mode with cURL and without geolocation" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27use_curl\x27 => false/\x27use_curl\x27 => true/g' scripts/auto-prepend/settings.php - sed -i 's/\x27forced_test_ip\x27 => \x27bad-ip\x27/\x27forced_test_ip\x27 => \x27\x27/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/1-live-mode.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "live mode with file_get_contents and with geolocation" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27use_curl\x27 => true/\x27use_curl\x27 => false/g' scripts/auto-prepend/settings.php - sed -i 's/\x27enabled\x27 => false/\x27enabled\x27 => true/g' scripts/auto-prepend/settings.php - sed -i 's/\x27forced_test_forwarded_ip\x27 => \x27\x27/\x27forced_test_forwarded_ip\x27 => \x27${{env.JP_TEST_IP}}\x27/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/2-live-mode-with-geolocation.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "live mode with cURL and with geolocation" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27use_curl\x27 => false/\x27use_curl\x27 => true/g' scripts/auto-prepend/settings.php - sed -i 's/\x27enabled\x27 => false/\x27enabled\x27 => true/g' scripts/auto-prepend/settings.php - sed -i 's/\x27forced_test_forwarded_ip\x27 => \x27\x27/\x27forced_test_forwarded_ip\x27 => \x27${{env.JP_TEST_IP}}\x27/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/2-live-mode-with-geolocation.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "stream mode with file_get_contents and without geolocation" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27use_curl\x27 => true/\x27use_curl\x27 => false/g' scripts/auto-prepend/settings.php - sed -i 's/\x27enabled\x27 => true/\x27enabled\x27 => false/g' scripts/auto-prepend/settings.php - sed -i 's/\x27forced_test_forwarded_ip\x27 => \x27${{env.JP_TEST_IP}}\x27/\x27forced_test_forwarded_ip\x27 => \x27\x27/g' scripts/auto-prepend/settings.php - sed -i 's/\x27stream_mode\x27 => false/\x27stream_mode\x27 => true/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/3-stream-mode.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "stream mode with cURL and without geolocation" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27use_curl\x27 => false/\x27use_curl\x27 => true/g' scripts/auto-prepend/settings.php - sed -i 's/\x27enabled\x27 => true/\x27enabled\x27 => false/g' scripts/auto-prepend/settings.php - sed -i 's/\x27forced_test_forwarded_ip\x27 => \x27${{env.JP_TEST_IP}}\x27/\x27forced_test_forwarded_ip\x27 => \x27\x27/g' scripts/auto-prepend/settings.php - sed -i 's/\x27stream_mode\x27 => false/\x27stream_mode\x27 => true/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/3-stream-mode.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "standalone geolocation" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/4-geolocation.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "live mode with IPv6" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27forced_test_forwarded_ip\x27 => \x27\x27/\x27forced_test_forwarded_ip\x27 => \x27${{env.IPV6_TEST_IP}}\x27/g' scripts/auto-prepend/settings.php - sed -i 's/\x27forced_test_ip\x27 => \x27\x27/\x27forced_test_ip\x27 => \x27${{env.IPV6_TEST_PROXY_IP}}\x27/g' scripts/auto-prepend/settings.php - sed -i -e 's/${{ env.PROXY_IP }}/${{env.IPV6_TEST_PROXY_IP}}/g' scripts/auto-prepend/settings.php - sed -i 's/\x27stream_mode\x27 => true/\x27stream_mode\x27 => false/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/1-live-mode.js" - - - name: Run "live mode with TLS auth" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27auth_type\x27 => \x27api_key\x27/\x27auth_type\x27 => \x27tls\x27/g' scripts/auto-prepend/settings.php - sed -i 's/\x27api_key\x27 => \x27${{env.BOUNCER_KEY}}\x27/\x27api_key\x27 => \x27\x27/g' scripts/auto-prepend/settings.php - sed -i 's/\x27tls_cert_path\x27 => \x27\x27/\x27tls_cert_path\x27 => \x27\/var\/www\/html\/cfssl\/bouncer.pem\x27/g' scripts/auto-prepend/settings.php - sed -i 's/\x27tls_key_path\x27 => \x27\x27/\x27tls_key_path\x27 => \x27\/var\/www\/html\/cfssl\/bouncer-key.pem\x27/g' scripts/auto-prepend/settings.php - sed -i 's/\x27tls_ca_cert_path\x27 => \x27\x27/\x27tls_ca_cert_path\x27 => \x27\/var\/www\/html\/cfssl\/ca-chain.pem\x27/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/1-live-mode.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "stream mode with TLS auth and cURL" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27stream_mode\x27 => false/\x27stream_mode\x27 => true/g' scripts/auto-prepend/settings.php - sed -i 's/\x27forced_test_forwarded_ip\x27 => \x27${{env.IPV6_TEST_IP}}\x27/\x27forced_test_forwarded_ip\x27 => \x27\x27/g' scripts/auto-prepend/settings.php - sed -i 's/\x27use_curl\x27 => false/\x27use_curl\x27 => true/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/3-stream-mode.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - - name: Run "stream mode with TLS auth and cURL and Redis" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27cache_system\x27 => Constants::CACHE_SYSTEM_PHPFS/\x27cache_system\x27 => Constants::CACHE_SYSTEM_REDIS/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/3-stream-mode.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - - - name: Run "stream mode with TLS auth and cURL and Memcached" test - run: | - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}} - sed -i 's/\x27cache_system\x27 => Constants::CACHE_SYSTEM_REDIS/\x27cache_system\x27 => Constants::CACHE_SYSTEM_MEMCACHED/g' scripts/auto-prepend/settings.php - cat scripts/auto-prepend/settings.php - cd ${{ github.workspace }}/${{env.EXTENSION_PATH}}/tests/end-to-end/ - ./__scripts__/run-tests.sh ci "./__tests__/3-stream-mode.js" - PENDING_TESTS=$(grep -oP '"numPendingTests":\K(.*),"numRuntimeErrorTestSuites"' .test-results.json | sed 's/,"numRuntimeErrorTestSuites"//g') - if [[ $PENDING_TESTS == "0" ]] - then - echo "No pending tests: OK" - else - echo "There are pending tests: $PENDING_TESTS (KO)" - exit 1 - fi - diff --git a/CHANGELOG.md b/CHANGELOG.md index 0825e29..1ccbd7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,18 @@ The [public API](https://semver.org/spec/v2.0.0.html#spec-item-1) of this librar --- -## [?.?.?](https://github.com/crowdsecurity/php-cs-bouncer/releases/tag/v?.?.?) - ?.?.? -[_Compare with previous release_](https://github.com/crowdsecurity/php-cs-bouncer/compare/v1.4.0...v?.?.?) +## [2.0.0](https://github.com/crowdsecurity/php-cs-bouncer/releases/tag/v2.0.0) - 2023-04-13 +[_Compare with previous release_](https://github.com/crowdsecurity/php-cs-bouncer/compare/v1.4.0...v2.0.0) ### Changed + - Update `gregwar/captcha` from `1.1.9` to `1.2.0` and remove some override fixes +### Removed + +- Remove all code about standalone bouncer + --- diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md index a820ada..6313fe1 100644 --- a/docs/DEVELOPER.md +++ b/docs/DEVELOPER.md @@ -69,7 +69,6 @@ For a quick start, follow the below steps. #### DDEV installation -This project is fully compatible with DDEV 1.21.4, and it is recommended to use this specific version. For the DDEV installation, please follow the [official instructions](https://ddev.readthedocs.io/en/stable/users/install/ddev-installation/). @@ -208,7 +207,7 @@ Finally, run ```bash ddev exec BOUNCER_KEY=your-bouncer-key AGENT_TLS_PATH=/var/www/html/cfssl LAPI_URL=https://crowdsec:8080 -MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-code/crowdsec-bouncer-lib/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./my-code/crowdsec-bouncer-lib/tests/Integration/IpVerificationTest.php +MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-code/crowdsec-bouncer-lib/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./my-code/crowdsec-bouncer-lib/tests/Integration/AbstractBouncerTest.php ``` For geolocation Unit Test, you should first put 2 free MaxMind databases in the `tests` folder : `GeoLite2-City.mmdb` @@ -222,70 +221,12 @@ ddev exec BOUNCER_KEY=your-bouncer-key AGENT_TLS_PATH=/var/www/html/cfssl LAPI_U MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-code/crowdsec-bouncer-lib/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./my-code/crowdsec-bouncer-lib/tests/Integration/GeolocationTest.php ``` -**N.B.**: If you want to test with `curl` instead of `file_get_contents` calls to LAPI, you have to add `USE_CURL=1` in -the previous commands. **N.B**.: If you want to test with `tls` authentification, you have to add `BOUNCER_TLS_PATH` environment variable and specify the path where you store certificates and keys. For example: ```bash -ddev exec USE_CURL=1 AGENT_TLS_PATH=/var/www/html/cfssl BOUNCER_TLS_PATH=/var/www/html/cfssl LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-code/crowdsec-bouncer-lib/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./my-code/crowdsec-bouncer-lib/tests/Integration/IpVerificationTest.php -``` - - -#### Auto-prepend mode (standalone mode) - -Before using the bouncer in a standalone mode (i.e. with an auto-prepend directive), you should copy the [`scripts/auto-prepend/settings.example.php`](../scripts/auto-prepend/settings.example.php) file to a `scripts/auto-prepend/settings.php` and edit it depending on your needs. - -Then, to configure the Nginx service in order that it uses an auto-prepend directive pointing to the [`scripts/auto-prepend/bounce.php`](../scripts/auto-prepend/bounce.php) script, please run the following command from the `.ddev` folder: - -```bash -ddev crowdsec-prepend-nginx -``` - -With that done, every access to your ddev url (i.e. `https://phpXX.ddev.site` where `XX` is your php version) will be bounce. - -For example, you should try to browse the following url: - -``` -https://phpXX.ddev.site/my-code/crowdsec-bouncer-lib/scripts/public/protected-page.php -``` - -#### End-to-end tests - -In auto-prepend mode, you can run some end-to-end tests. - -We are using a Jest/Playwright Node.js stack to launch a suite of end-to-end tests. - -Tests code is in the `tests/end-to-end` folder. You should have to `chmod +x` the scripts you will find in `tests/end-to-end/__scripts__`. - - -``` -cd crowdsec-bouncer-project -cp -r .ddev/okaeli-add-on/custom_files/crowdsec/cfssl/* cfssl -``` - -Then you can use the `run-test.sh` script to run the tests: - -- the first parameter specifies if you want to run the test on your machine (`host`) or in the - docker containers (`docker`). You can also use `ci` if you want to have the same behavior as in GitHub action. -- the second parameter list the test files you want to execute. If empty, all the test suite will be launched. - -For example: - - ./run-tests.sh host "./__tests__/1-live-mode.js" - ./run-tests.sh docker "./__tests__/1-live-mode.js" - ./run-tests.sh host - -Before testing with the `docker` or `ci` parameter, you have to install all the required dependencies in the playwright container with this command : - - ./test-init.sh - -If you want to test with the `host` parameter, you will have to install manually all the required dependencies: - -```bash -yarn --cwd ./tests/end-to-end --force -yarn global add cross-env +ddev exec USE_CURL=1 AGENT_TLS_PATH=/var/www/html/cfssl BOUNCER_TLS_PATH=/var/www/html/cfssl LAPI_URL=https://crowdsec:8080 MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-code/crowdsec-bouncer-lib/vendor/bin/phpunit --testdox --colors --exclude-group ignore ./my-code/crowdsec-bouncer-lib/tests/Integration/AbstractBouncerTest.php ``` #### Coding standards @@ -331,13 +272,13 @@ ddev phpmd ./my-code/crowdsec-bouncer-lib/tools/coding-standards phpmd/rulesets. To use [PHP Code Sniffer](https://github.com/squizlabs/PHP_CodeSniffer) tools, you can run: ```bash -ddev phpcs ./my-code/crowdsec-bouncer-lib/tools/coding-standards my-code/crowdsec-php-lib/src PSR12 +ddev phpcs ./my-code/crowdsec-bouncer-lib/tools/coding-standards my-code/crowdsec-bouncer-lib/src PSR12 ``` and: ```bash -ddev phpcbf ./my-code/crowdsec-php-lib/tools/coding-standards my-code/crowdsec-php-lib/src PSR12 +ddev phpcbf ./my-code/crowdsec-bouncer-lib/tools/coding-standards my-code/crowdsec-bouncer-lib/src PSR12 ``` @@ -346,7 +287,7 @@ ddev phpcbf ./my-code/crowdsec-php-lib/tools/coding-standards my-code/crowdsec- To use [PSALM](https://github.com/vimeo/psalm) tools, you can run: ```bash -ddev psalm ./my-code/crowdsec-php-lib/tools/coding-standards ./my-code/crowdsec-php-lib/tools/coding-standards/psalm +ddev psalm ./my-code/crowdsec-bouncer-lib/tools/coding-standards ./my-code/crowdsec-bouncer-lib/tools/coding-standards/psalm ``` ##### PHP Unit Code coverage @@ -360,7 +301,7 @@ ddev xdebug To generate a html report, you can run: ```bash -ddev exec XDEBUG_MODE=coverage BOUNCER_KEY=your-bouncer-key AGENT_TLS_PATH=/var/www/html/cfssl LAPI_URL=https://crowdsec:8080 REDIS_DSN=redis://redis:6379 MEMCACHED_DSN=memcached://memcached:11211 /usr/bin/php ./my-code/crowdsec-php-lib/tools/coding-standards/vendor/bin/phpunit --configuration ./my-code/crowdsec-php-lib/tools/coding-standards/phpunit/phpunit.xml +ddev exec XDEBUG_MODE=coverage BOUNCER_KEY=your-bouncer-key AGENT_TLS_PATH=/var/www/html/cfssl LAPI_URL=https://crowdsec:8080 REDIS_DSN=redis://redis:6379 MEMCACHED_DSN=memcached://memcached:11211 /usr/bin/php ./my-code/crowdsec-bouncer-lib/tools/coding-standards/vendor/bin/phpunit --configuration ./my-code/crowdsec-bouncer-lib/tools/coding-standards/phpunit/phpunit.xml ``` @@ -371,7 +312,7 @@ If you want to generate a text report in the same folder: ```bash ddev exec XDEBUG_MODE=coverage BOUNCER_KEY=your-bouncer-key LAPI_URL=https://crowdsec:8080 -MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-code/crowdsec-php-lib/tools/coding-standards/vendor/bin/phpunit --configuration ./my-code/crowdsec-php-lib/tools/coding-standards/phpunit/phpunit.xml --coverage-text=./my-code/crowdsec-php-lib/tools/coding-standards/phpunit/code-coverage/report.txt +MEMCACHED_DSN=memcached://memcached:11211 REDIS_DSN=redis://redis:6379 /usr/bin/php ./my-code/crowdsec-bouncer-lib/tools/coding-standards/vendor/bin/phpunit --configuration ./my-code/crowdsec-bouncer-lib/tools/coding-standards/phpunit/phpunit.xml --coverage-text=./my-code/crowdsec-bouncer-lib/tools/coding-standards/phpunit/code-coverage/report.txt ``` #### Generate CrowdSec tools and settings on start @@ -434,72 +375,6 @@ the max number of keys to dump: - `delete `: Delete a key -## Example scripts - -You will find some php scripts in the `scripts` folder. - -**N.B**. : If you are not using DDEV, you can replace all `ddev exec php ` by `php` and specify the right script paths. - -### Clear cache script - -To clear your LAPI cache, you can use the [`clear-php`](../scripts/clear-cache.php) script: - -```bash -ddev exec php my-code/crowdsec-php-lib/scripts/clear-cache.php -``` - -### Full Live mode example - -This example demonstrates how the PHP Lib works with cache when you are using the live mode. - -We will use here the [`standalone-check-ip-live.php`](../scripts/standalone-check-ip-live.php). - -#### Set up the context - -Start the containers: - -```bash -ddev start -``` - -Then get a bouncer API key by copying the result of: - -```bash -ddev create-bouncer -``` - -#### Get the remediation the clean IP "1.2.3.4" - -Try with the `standalone-check-ip-live.php` file: - - -```bash -ddev exec php my-code/crowdsec-php-lib/scripts/standalone-check-ip-live.php 1.2.3.4 -``` - -#### Now ban range 1.2.3.4 to 1.2.3.7 for 12h - -```bash -ddev exec -s crowdsec cscli decisions add --range 1.2.3.4/30 --duration 12h --type ban -``` - -#### Clear cache and get the new remediation - -Clear the cache: - -```bash -ddev exec php my-code/crowdsec-php-lib/scripts/clear-cache.php -``` - -One more time, get the remediation for the IP "1.2.3.4": - -```bash -ddev exec php my-code/crowdsec-php-lib/scripts/standalone-check-ip-live.php 1.2.3.4 -``` - -This is a ban (and cache miss) as you can see in your terminal logs. - - ## Discover the CrowdSec LAPI This library interacts with a CrowdSec agent that you have installed on an accessible server. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 7050494..0ccaa5d 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -41,8 +41,7 @@ ## Description This library allows you to create CrowdSec bouncers for PHP applications or frameworks like e-commerce, blog or other -exposed applications. It can also be used in a standalone mode using auto-prepend directive and the provided standalone -bouncer. +exposed applications. ## Prerequisites @@ -54,11 +53,11 @@ Please note that first and foremost a CrowdSec agent must be installed on a serv - CrowdSec Local API Support - Handle `ip`, `range` and `country` scoped decisions - - Clear, prune and refresh the Local API cache - `Live mode` or `Stream mode` - Support IpV4 and Ipv6 (Ipv6 range decisions are yet only supported in `Live mode`) - Large PHP matrix compatibility: 7.2, 7.3, 7.4, 8.0, 8.1 and 8.2 - Built-in support for the most known cache systems Redis, Memcached and PhpFiles + - Clear, prune and refresh the bouncer cache - Cap remediation level (ex: for sensitives websites: ban will be capped to captcha) @@ -76,140 +75,10 @@ A captcha wall could look like: ![Captcha wall](images/screenshots/front-captcha.jpg) -With the provided standalone bouncer, please note that it is possible to customize all the colors of these pages so that they integrate best with your design. +Please note that it is possible to customize all the colors of these pages so that they integrate best with your design. On the other hand, all texts are also fully customizable. This will allow you, for example, to present translated pages in your users' language. - -## Standalone bouncer set up - -This library includes the [`StandaloneBouncer`](../src/StandaloneBouncer.php) class. You can see that class as a good example for creating your own bouncer. - -Once you set up your server as below, every browser access to a php script will be bounced by the standalone bouncer. - -You will have to : - -- copy sources of the lib in some `/path/to/the/crowdsec-lib` folder - -- give the correct permission for the folder that contains the lib - -- copy the `scripts/auto-prepend/settings.example.php` to a `scripts/auto-prepend/settings.php` file - -- run the composer installation process to retrieve all necessary dependencies - -- set an `auto_prepend_file` directive in your PHP setup. - -- Optionally, if you want to use the standalone bouncer in stream mode, you wil have to set a cron task to refresh - cache periodically. - -### Copy sources - -Create a folder `crowdsec-lib` and clone the sources: - -```shell -mkdir -p /path/to/the/crowdsec-lib && git clone git@github.com:crowdsecurity/php-cs-bouncer.git /path/to/the/crowdsec-lib -``` - -### Files permission - -The owner of the `/path/to/the/crowdsec-lib` should be your webserver owner (e.g. `www-data`). - -You can achieve it by running command like: - -```shell -sudo chown www-data /path/to/the/crowdsec-lib -``` - -### Composer - -You should run the composer installation process: - -```shell -cd /path/to/the/crowdsec-lib && composer install -``` - -### Settings file - -Please copy the `scripts/auto-prepend/settings.example.php` to a `scripts/auto-prepend/settings.php` and fill the necessary settings in it (see [Configurations settings](#configurations) for more details). - -### `auto_prepend_file` directive - -We will now describe how to set an `auto_prepend_file` directive in order to call the `scripts/auto-prepend/bounce.php` for each php script access. - -Adding an `auto_prepend_file` directive can be done in different ways: - -#### `.ini` file - -You should add this line to a `.ini` file : - - auto_prepend_file = /absolute/path/to/scripts/auto-prepend/bounce.php - -#### Nginx - -If you are using Nginx, you should modify your Nginx configuration file by adding a `fastcgi_param` directive. The php block should look like below: - -``` -location ~ \.php$ { - ... - ... - ... - fastcgi_param PHP_VALUE "/absolute/path/to/scripts/auto-prepend/bounce.php"; -} -``` - -#### Apache - -If you are using Apache, you should add this line to your `.htaccess` file: - - php_value auto_prepend_file "/absolute/path/to/scripts/auto-prepend/bounce.php" - -or modify your `Virtual Host` accordingly: - -``` - - ... - ... - php_value auto_prepend_file "/absolute/path/to/scripts/auto-prepend/bounce.php" - - -``` - - -### Stream mode cron task - -To use the stream mode, you first have to set the `stream_mode` setting value to `true` in your `scripts/auto-prepend/settings.php` file. - -Then, you could edit the web server user (e.g. `www-data`) crontab: - -```shell -sudo -u www-data crontab -e -``` - -and add the following line - -```shell -* * * * * /usr/bin/php /absolute/path/to/scripts/auto-prepend/refresh-cache.php -``` - -In this example, cache is refreshed every minute, but you can modify the cron expression depending on your needs. - -### Cache pruning cron task - -To use the PHP file system as cache, you should prune the cache with a cron job: - -```shell -sudo -u www-data crontab -e -``` - -and add the following line - -```shell -0 0 * * * /usr/bin/php /absolute/path/to/scripts/auto-prepend/prune-cache.php -``` - -In this example, cache is pruned at midnight every day, but you can modify the cron expression depending on your needs. - - ## Create your own bouncer ### Implementation @@ -279,7 +148,9 @@ class MyCustomBouncer extends AbstractBouncer ``` -Once you have implemented these methods, you could retrieve all required configurations to instantiate your bouncer and then call the `run` method to apply a bounce for the current detected IP. +Once you have implemented these methods, you could retrieve all required configurations to instantiate your bouncer +and then call the `run` method to apply a bounce for the current detected IP. Please see below for the list of +available configurations. In order to instantiate the bouncer, you will have to create at least a `CrowdSec\RemediationEngine\LapiRemediation` object too. @@ -292,14 +163,13 @@ use CrowdSec\LapiClient\Bouncer as BouncerClient; use CrowdSec\RemediationEngine\CacheStorage\PhpFiles; $configs = [...]; -$client = new BouncerClient($configs); -$cacheStorage = new PhpFiles($configs); +$client = new BouncerClient($configs);// @see AbstractBouncer::handleClient method for a basic client creation +$cacheStorage = new PhpFiles($configs);// @see AbstractBouncer::handleCache method for a basic cache storage creation $remediationEngine = new LapiRemediation($configs, $client, $cacheStorage); $bouncer = new MyCustomBouncer($configs, $remediationEngine); $bouncer->run(); - ``` @@ -323,8 +193,6 @@ To go further and learn how to include this library in your project, you should ## Configurations -You can pass an array of configurations in the bouncer constructor. Please look at the [Settings example file](../scripts/auto-prepend/settings.example.php) for quick overview. - Here is the list of available settings: ### Bouncer behavior @@ -335,7 +203,8 @@ Here is the list of available settings: - `fallback_remediation`: Select from `bypass` (minimum remediation), `captcha` or `ban` (maximum remediation). Default to 'captcha'. Handle unknown remediations as. -- `trust_ip_forward_array`: If you use a CDN, a reverse proxy or a load balancer, set an array of IPs. For other IPs, the bouncer will not trust the X-Forwarded-For header. +- `trust_ip_forward_array`: If you use a CDN, a reverse proxy or a load balancer, set an array of comparable IPs arrays: + (example: `[['001.002.003.004', '001.002.003.004'], ['005.006.007.008', '005.006.007.008']]` for CDNs with IPs `1.2.3.4` and `5.6.7.8`). For other IPs, the bouncer will not trust the X-Forwarded-For header. - `excluded_uris`: array of URIs that will not be bounced. @@ -355,10 +224,12 @@ Here is the list of available settings: - `tls_cert_path`: absolute path to the bouncer certificate (e.g. pem file). Only required if you choose `tls` as `auth_type`. + **Make sure this path is not publicly accessible.** [See security note below](#security-note). - `tls_key_path`: Absolute path to the bouncer key (e.g. pem file). Only required if you choose `tls` as `auth_type`. + **Make sure this path is not publicly accessible.** [See security note below](#security-note). - `tls_verify_peer`: This option determines whether request handler verifies the authenticity of the peer's certificate. @@ -371,6 +242,7 @@ Here is the list of available settings: - `tls_ca_cert_path`: Absolute path to the CA used to process peer verification. Only required if you choose `tls` as `auth_type` and `tls_verify_peer` is set to true. + **Make sure this path is not publicly accessible.** [See security note below](#security-note). - `api_url`: Define the URL to your Local API server, default to `http://localhost:8080`. @@ -380,22 +252,17 @@ Here is the list of available settings: timeout will be unlimited. -- `use_curl`: By default, this lib call the REST Local API using `file_get_contents` method (`allow_url_fopen` is required). - You can set `use_curl` to `true` in order to use `cURL` request instead (`curl` is in then required) - ### Cache -- `cache_system`: Select from `phpfs` (PHP file cache), `redis` or `memcached`. +- `fs_cache_path`: Will be used only if you choose PHP file cache as cache storage. + **Make sure this path is not publicly accessible.** [See security note below](#security-note). -- `fs_cache_path`: Will be used only if you choose PHP file cache as `cache_system`. Important note: be sur this path - won't be publicly accessible. +- `redis_dsn`: Will be used only if you choose Redis cache as cache storage. -- `redis_dsn`: Will be used only if you choose Redis cache as `cache_system`. - -- `memcached_dsn`: Will be used only if you choose Memcached as `cache_system`. +- `memcached_dsn`: Will be used only if you choose Memcached as cache storage. - `clean_ip_cache_duration`: Set the duration we keep in cache the fact that an IP is clean. In seconds. Defaults to 5. @@ -424,6 +291,7 @@ Here is the list of available settings: - `geolocation[maxmind][database_type]`: Select from `country` or `city`. Default to `country`. These are the two available MaxMind database types. - `geolocation[maxmind][database_path]`: Absolute path to the MaxMind database (e.g. mmdb file) + **Make sure this path is not publicly accessible.** [See security note below](#security-note). ### Captcha and ban wall settings @@ -488,8 +356,8 @@ Here is the list of available settings: - `disable_prod_log`: `true` to disable prod log. Default to `false`. -- `log_directory_path`: Absolute path to store log files. Important note: be sure this path won't be publicly - accessible. +- `log_directory_path`: Absolute path to store log files. + **Make sure this path is not publicly accessible.** [See security note below](#security-note). - `display_errors`: true to stop the process and display errors on browser if any. @@ -502,12 +370,48 @@ Here is the list of available settings: - `forced_test_forwarded_ip`: Only for test or debug purpose. Default to empty. If not empty, it will be used instead of the real forwarded ip. If set to `no_forward`, the x-forwarded-for mechanism will not be used at all. +### Security note + +Some files should not be publicly accessible because they may contain sensitive data: +- Log files +- Cache files of the File system cache +- TLS authentication files +- Geolocation database files + +If you define publicly accessible folders in the settings, be sure to add rules to deny access to these files. + +In the following example, we will suppose that you use a folder `crowdsec` with sub-folders `logs`, `cache`, `tls` and `geolocation`. + +If you are using Nginx, you could use the following snippet to modify your website configuration file: + +```nginx +server { + ... + ... + ... + # Deny all attempts to access some folders of the crowdsec bouncer lib + location ~ /crowdsec/(logs|cache|tls|geolocation) { + deny all; + } + ... + ... +} +``` + +If you are using Apache, you could add this kind of directive in a `.htaccess` file: + +```htaccess +Redirectmatch 403 crowdsec/logs/ +Redirectmatch 403 crowdsec/cache/ +Redirectmatch 403 crowdsec/tls/ +Redirectmatch 403 crowdsec/geolocation/ +``` ## Other ready to use PHP bouncers -To have a more concrete idea on how to develop a bouncer, you may look at the following bouncers for Magento 2 and -WordPress : +To have a more concrete idea on how to develop a bouncer, you may look at the following bouncers : - [CrowdSec Bouncer extension for Magento 2](https://github.com/crowdsecurity/cs-magento-bouncer) - [CrowdSec Bouncer plugin for WordPress ](https://github.com/crowdsecurity/cs-wordpress-bouncer) +- [CrowdSec Standalone Bouncer ](https://github.com/crowdsecurity/cs-php-bouncer) diff --git a/scripts/auto-prepend/bounce.php b/scripts/auto-prepend/bounce.php deleted file mode 100644 index d88c696..0000000 --- a/scripts/auto-prepend/bounce.php +++ /dev/null @@ -1,24 +0,0 @@ -run(); -} catch (\Throwable $e) { - $displayErrors = $crowdSecStandaloneBouncerConfig['display_errors'] ?? false; - if (true === $displayErrors) { - throw new BouncerException($e->getMessage(), (int) $e->getCode(), $e); - } -} diff --git a/scripts/auto-prepend/prune-cache.php b/scripts/auto-prepend/prune-cache.php deleted file mode 100644 index 31055da..0000000 --- a/scripts/auto-prepend/prune-cache.php +++ /dev/null @@ -1,15 +0,0 @@ -pruneCache(); diff --git a/scripts/auto-prepend/refresh-cache.php b/scripts/auto-prepend/refresh-cache.php deleted file mode 100644 index bdfe525..0000000 --- a/scripts/auto-prepend/refresh-cache.php +++ /dev/null @@ -1,15 +0,0 @@ -refreshBlocklistCache(); diff --git a/scripts/auto-prepend/settings.example.php b/scripts/auto-prepend/settings.example.php deleted file mode 100644 index b3abc35..0000000 --- a/scripts/auto-prepend/settings.example.php +++ /dev/null @@ -1,244 +0,0 @@ - Constants::BOUNCING_LEVEL_NORMAL, - - /** If you use a CDN, a reverse proxy or a load balancer, you can use this setting to whitelist their IPs. - * - * For other IPs, the bouncer will not trust the X-Forwarded-For header. - * - * With the Standalone bouncer, you have to set an array of Ips : ['1.2.3.4', '5.6.7.8'] for example - * The standalone bouncer will automatically transform this array to an array of comparable IPs arrays: - * [['001.002.003.004', '001.002.003.004'], ['005.006.007.008', '005.006.006.007']] - * - * If you use your own bouncer, you should have to set directly an array of comparable IPs arrays - */ - 'trust_ip_forward_array' => [], - /** - * By default, the lib call the REST LAPI using file_get_contents method (allow_url_fopen is required). - * Set 'use_curl' to true in order to use cURL request instead (curl is in then required). - */ - 'use_curl' => false, - - /** - * array of URIs that will not be bounced. - */ - 'excluded_uris' => ['/favicon.ico'], - - // Select from 'phpfs' (File system cache), 'redis' or 'memcached'. - 'cache_system' => Constants::CACHE_SYSTEM_PHPFS, - - // Set the duration we keep in cache the captcha flow variables for an IP. In seconds. Defaults to 86400. - 'captcha_cache_duration' => Constants::CACHE_EXPIRATION_FOR_CAPTCHA, - - // true to enable verbose debug log. - 'debug_mode' => false, - // true to disable prod log - 'disable_prod_log' => false, - - /** Absolute path to store log files. - * - * Important note: be sur this path won't be publicly accessible - */ - 'log_directory_path' => __DIR__ . '/.logs', - - // true to stop the process and display errors if any. - 'display_errors' => false, - - /** Only for test or debug purpose. Default to empty. - * - * If not empty, it will be used instead of the real remote ip. - */ - 'forced_test_ip' => '', - - /** Only for test or debug purpose. Default to empty. - * - * If not empty, it will be used instead of the real forwarded ip. - * If set to "no_forward", the x-forwarded-for mechanism will not be used at all. - */ - 'forced_test_forwarded_ip' => '', - - // Settings for ban and captcha walls - 'custom_css' => '', - // true to hide CrowdSec mentions on ban and captcha walls. - 'hide_mentions' => false, - 'color' => [ - 'text' => [ - 'primary' => 'black', - 'secondary' => '#AAA', - 'button' => 'white', - 'error_message' => '#b90000', - ], - 'background' => [ - 'page' => '#eee', - 'container' => 'white', - 'button' => '#626365', - 'button_hover' => '#333', - ], - ], - 'text' => [ - // Settings for captcha wall - 'captcha_wall' => [ - 'tab_title' => 'Oops..', - 'title' => 'Hmm, sorry but...', - 'subtitle' => 'Please complete the security check.', - 'refresh_image_link' => 'refresh image', - 'captcha_placeholder' => 'Type here...', - 'send_button' => 'CONTINUE', - 'error_message' => 'Please try again.', - 'footer' => '', - ], - // Settings for ban wall - 'ban_wall' => [ - 'tab_title' => 'Oops..', - 'title' => '🤭 Oh!', - 'subtitle' => 'This page is protected against cyber attacks and your IP has been banned by our system.', - 'footer' => '', - ], - ], - - // ============================================================================# - // Client configs - // ============================================================================# - - /** Select from 'api_key' and 'tls'. - * - * Choose if you want to use an API-KEY or a TLS (pki) authentification - * TLS authentication is only available if you use CrowdSec agent with a version superior to 1.4.0 - */ - 'auth_type' => Constants::AUTH_KEY, - - /** Absolute path to the bouncer certificate. - * - * Only required if you choose tls as "auth_type" - */ - 'tls_cert_path' => '', - - /** Absolute path to the bouncer key. - * - * Only required if you choose tls as "auth_type" - */ - 'tls_key_path' => '', - - /** This option determines whether request handler verifies the authenticity of the peer's certificate. - * - * When negotiating a TLS or SSL connection, the server sends a certificate indicating its identity. - * If "tls_verify_peer" is set to true, request handler verifies whether the certificate is authentic. - * This trust is based on a chain of digital signatures, - * rooted in certification authority (CA) certificates you supply using the "tls_ca_cert_path" setting below. - */ - 'tls_verify_peer' => true, - - /** Absolute path to the CA used to process peer verification. - * - * Only required if you choose tls as "auth_type" and "tls_verify_peer" is true - */ - 'tls_ca_cert_path' => '', - - /** The bouncer api key to access LAPI. - * - * Key generated by the cscli (CrowdSec cli) command like "cscli bouncers add bouncer-php-library" - * Only required if you choose api_key as "auth_type" - */ - 'api_key' => 'YOUR_BOUNCER_API_KEY', - - /** Define the URL to your LAPI server, default to http://localhost:8080. - * - * If you have installed the CrowdSec agent on your server, it should be "http://localhost:8080" or - * "https://localhost:8080" - */ - 'api_url' => Constants::DEFAULT_LAPI_URL, - - // In seconds. The timeout when calling LAPI. Must be greater or equal than 1. Defaults to 1 sec. - 'api_timeout' => Constants::API_TIMEOUT, - - // ============================================================================# - // Remediation engine configs - // ============================================================================# - - /** Select from 'bypass' (minimum remediation), 'captcha' or 'ban' (maximum remediation). - * Default to 'captcha'. - * - * Handle unknown remediations as. - */ - 'fallback_remediation' => Constants::REMEDIATION_CAPTCHA, - - /** - * The `ordered_remediations` setting accepts an array of remediations ordered by priority. - * If there are more than one decision for an IP, remediation with the highest priority will be return. - * The specific remediation `bypass` will always be considered as the lowest priority (there is no need to - * specify it in this setting). - * This setting is not required. If you don't set any value, `['ban']` will be used by default for CAPI remediation - * and `['ban', 'captcha']` for LAPI remediation. - */ - 'ordered_remediations' => [Constants::REMEDIATION_BAN, Constants::REMEDIATION_CAPTCHA], - - /** Will be used only if you choose File system as cache_system. - * - * Important note: be sur this path won't be publicly accessible - */ - 'fs_cache_path' => __DIR__ . '/.cache', - - // Will be used only if you choose Redis cache as cache_system - 'redis_dsn' => 'redis://localhost:6379', - - // Will be used only if you choose Memcached as cache_system - 'memcached_dsn' => 'memcached://localhost:11211', - - // Set the duration we keep in cache the fact that an IP is clean. In seconds. Defaults to 5. - 'clean_ip_cache_duration' => Constants::CACHE_EXPIRATION_FOR_CLEAN_IP, - - // Set the duration we keep in cache the fact that an IP is bad. In seconds. Defaults to 20. - 'bad_ip_cache_duration' => Constants::CACHE_EXPIRATION_FOR_BAD_IP, - - /** true to enable stream mode, false to enable the live mode. Default to false. - * - * By default, the `live mode` is enabled. The first time a stranger connects to your website, this mode - * means that the IP will be checked directly by the CrowdSec API. The rest of your user’s browsing will be - * even more transparent thanks to the fully customizable cache system. - * - * But you can also activate the `stream mode`. This mode allows you to constantly feed the bouncer with the - * malicious IP list via a background task (CRON), making it to be even faster when checking the IP of your - * visitors. Besides, if your site has a lot of unique visitors at the same time, this will not influence the - * traffic to the API of your CrowdSec instance. - */ - 'stream_mode' => false, - - // Settings for geolocation remediation (i.e. country based remediation). - 'geolocation' => [ - // true to enable remediation based on country. Default to false. - 'enabled' => false, - // Geolocation system. Only 'maxmind' is available for the moment. Default to 'maxmind' - 'type' => Constants::GEOLOCATION_TYPE_MAXMIND, - /** - * This setting will be used to set the lifetime (in seconds) of a cached country - * associated to an IP. The purpose is to avoid multiple call to the geolocation system (e.g. maxmind database) - * . Default to 86400. Set 0 to disable caching. - */ - 'cache_duration' => Constants::CACHE_EXPIRATION_FOR_GEO, - // MaxMind settings - 'maxmind' => [ - /**Select from 'country' or 'city'. Default to 'country' - * - * These are the two available MaxMind database types. - */ - 'database_type' => Constants::MAXMIND_COUNTRY, - // Absolute path to the MaxMind database (mmdb file). - 'database_path' => '/some/path/GeoLite2-Country.mmdb', - ], - ], -]; diff --git a/scripts/clear-cache.php b/scripts/clear-cache.php deleted file mode 100644 index 0eba451..0000000 --- a/scripts/clear-cache.php +++ /dev/null @@ -1,38 +0,0 @@ -'); -} -echo "\nClear the cache...\n"; - -// Instantiate the Stream logger -$logger = new Logger('example'); - -// Display logs with DEBUG verbosity -$streamHandler = new StreamHandler('php://stdout', Logger::DEBUG); -$streamHandler->setFormatter(new LineFormatter("[%datetime%] %message% %context%\n")); -$logger->pushHandler($streamHandler); - - -// Instantiate the bouncer -$configs = [ - 'api_key' => $bouncerApiKey, - 'api_url' => 'https://crowdsec:8080', - 'fs_cache_path' => __DIR__ . '/.cache', -]; -$bouncer = new StandaloneBouncer($configs, $logger); - -// Clear the cache. -$bouncer->clearCache(); -echo "Cache successfully cleared.\n"; diff --git a/scripts/public/cache-actions.php b/scripts/public/cache-actions.php deleted file mode 100644 index 06a3b6d..0000000 --- a/scripts/public/cache-actions.php +++ /dev/null @@ -1,63 +0,0 @@ -Cache action has been done: $action"; - - switch ($action) { - case 'refresh': - $bouncer->refreshBlocklistCache(); - break; - case 'clear': - $bouncer->clearCache(); - break; - case 'prune': - $bouncer->pruneCache(); - break; - case 'captcha-phrase': - if(!isset($_GET['ip'])){ - exit('You must pass an "ip" param to get the associated captcha phrase' . \PHP_EOL); - } - $ip = $_GET['ip']; - $cache = $bouncer->getRemediationEngine()->getCacheStorage(); - $cacheKey = $cache->getCacheKey(Constants::CACHE_TAG_CAPTCHA, $ip); - $item = $cache->getItem($cacheKey); - $result = "

No captcha for this IP: $ip

"; - if($item->isHit()){ - $cached = $item->get(); - $phrase = $cached['phrase_to_guess']??"No phrase to guess for this captcha (already resolved ?)"; - $result = "

$phrase

"; - } - break; - default: - throw new Exception("Unknown cache action type:$action"); - } - - echo " - - - - - Cache action: $action - - - - $result - - -"; -} else { - exit('You must pass an "action" param (refresh, clear or prune)' . \PHP_EOL); -} diff --git a/scripts/public/geolocation-test.php b/scripts/public/geolocation-test.php deleted file mode 100644 index daaf753..0000000 --- a/scripts/public/geolocation-test.php +++ /dev/null @@ -1,68 +0,0 @@ - true, - 'cache_duration' => $cacheDuration, - 'type' => 'maxmind', - 'maxmind' => [ - 'database_type' => $dbType, - 'database_path' => '/var/www/html/my-code/crowdsec-bouncer-lib/tests/' . $dbName, - ], - ]; - - if ($fakeBrokenDb) { - $geolocConfig['maxmind']['database_path'] = '/var/www/html/my-code/crowdsec-bouncer-lib/tests/broken.mmdb'; - } - - $finalConfig = array_merge($crowdSecStandaloneBouncerConfig, ['geolocation' => $geolocConfig]); - $bouncer = new StandaloneBouncer($finalConfig); - - $cache = $bouncer->getRemediationEngine()->getCacheStorage(); - - $geolocation = new Geolocation($geolocConfig, $cache, $bouncer->getLogger()); - if ($cacheDuration <= 0) { - $geolocation->clearGeolocationCache($requestedIp); - } - - $countryResult = $geolocation->handleCountryResultForIp($requestedIp); - $country = $countryResult['country']; - $notFound = $countryResult['not_found']; - $error = $countryResult['error']; - - echo " - - - - - Geolocation for IP: $requestedIp - - - -

For IP $requestedIp:

-
    -
  • Country: $country
  • -
  • Not Found message: $notFound
  • -
  • Error message: $error
  • -
  • Cache duration: $cacheDuration
  • -
- - -"; -} else { - exit('You must pass an "ip" param' . \PHP_EOL); -} diff --git a/scripts/public/protected-page.php b/scripts/public/protected-page.php deleted file mode 100644 index e37fc94..0000000 --- a/scripts/public/protected-page.php +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Home page - - - -

The way is clear!

-

In this example page, if you can see this text, the bouncer considers your IP as clean.

- - -'; diff --git a/scripts/refresh-cache-stream.php b/scripts/refresh-cache-stream.php deleted file mode 100644 index 43f21c5..0000000 --- a/scripts/refresh-cache-stream.php +++ /dev/null @@ -1,35 +0,0 @@ -'); -} - -// Instantiate the Stream logger -$logger = new Logger('example'); - -// Display logs with DEBUG verbosity -$streamHandler = new StreamHandler('php://stdout', Logger::DEBUG); -$streamHandler->setFormatter(new LineFormatter("[%datetime%] %message% %context%\n")); -$logger->pushHandler($streamHandler); - -// Instantiate the bouncer -$configs = [ - 'api_key' => $bouncerKey, - 'api_url' => 'https://crowdsec:8080', - 'fs_cache_path' => __DIR__ . '/.cache', - 'stream_mode' => true, -]; -$bouncer = new StandaloneBouncer($configs, $logger); - -// Refresh the blocklist cache -$bouncer->refreshBlocklistCache(); -echo "Cache successfully refreshed.\n"; diff --git a/scripts/standalone-check-ip-live.php b/scripts/standalone-check-ip-live.php deleted file mode 100644 index 214d865..0000000 --- a/scripts/standalone-check-ip-live.php +++ /dev/null @@ -1,43 +0,0 @@ - '); -} -// Instantiate the Stream logger -$logger = new Logger('example'); - -// Display logs with DEBUG verbosity -$streamHandler = new StreamHandler('php://stdout', Logger::DEBUG); -$streamHandler->setFormatter(new LineFormatter("[%datetime%] %message% %context%\n")); -$logger->pushHandler($streamHandler); - -// Init -$configs = [ - 'api_key' => $bouncerKey, - 'api_url' => 'https://crowdsec:8080', - 'fs_cache_path' => __DIR__ . '/.cache', - 'stream_mode' => false -]; -$bouncer = new StandaloneBouncer($configs, $logger); - -// Ask remediation to LAPI - -echo "\nVerify $requestedIp...\n"; -$remediation = $bouncer->getRemediationForIp($requestedIp); -echo "\nResult: $remediation\n\n"; // "ban", "captcha" or "bypass" - - - - diff --git a/src/AbstractBouncer.php b/src/AbstractBouncer.php index 6862311..f21581f 100644 --- a/src/AbstractBouncer.php +++ b/src/AbstractBouncer.php @@ -64,15 +64,14 @@ public function __construct( 'type' => 'BOUNCER_INIT', 'logger' => \get_class($this->getLogger()), 'remediation' => \get_class($this->getRemediationEngine()), - 'configs' => $configs + 'configs' => $configs, ]); } /** * Apply a bounce for current IP depending on remediation associated to this IP - * (e.g. display a ban wall, captcha wall or do nothing in case of a bypass) + * (e.g. display a ban wall, captcha wall or do nothing in case of a bypass). * - * @return void * @throws BouncerException * @throws CacheException * @throws InvalidArgumentException @@ -96,7 +95,6 @@ public function bounceCurrentIp(): void * * @return bool If the cache has been successfully cleared or not * - * * @throws BouncerException */ public function clearCache(): bool @@ -104,14 +102,13 @@ public function clearCache(): bool try { return $this->getRemediationEngine()->clearCache(); } catch (\Exception $e) { - throw new BouncerException('Error while clearing cache: ' . $e->getMessage(), (int)$e->getCode(), $e); + throw new BouncerException('Error while clearing cache: ' . $e->getMessage(), (int) $e->getCode(), $e); } } /** - * Retrieve Bouncer configuration by name + * Retrieve Bouncer configuration by name. * - * @param string $name * @return mixed */ public function getConfig(string $name) @@ -120,9 +117,7 @@ public function getConfig(string $name) } /** - * Retrieve Bouncer configurations - * - * @return array + * Retrieve Bouncer configurations. */ public function getConfigs(): array { @@ -130,12 +125,12 @@ public function getConfigs(): array } /** - * Get current http method + * Get current http method. */ abstract public function getHttpMethod(): string; /** - * Get value of an HTTP request header. Ex: "X-Forwarded-For" + * Get value of an HTTP request header. Ex: "X-Forwarded-For". */ abstract public function getHttpRequestHeader(string $name): ?string; @@ -163,6 +158,7 @@ public function getRemediationEngine(): AbstractRemediation * Get the remediation for the specified IP. * * @return string the remediation to apply (ex: 'ban', 'captcha', 'bypass') + * * @throws BouncerException */ public function getRemediationForIp(string $ip): string @@ -170,17 +166,17 @@ public function getRemediationForIp(string $ip): string try { return $this->capRemediationLevel($this->getRemediationEngine()->getIpRemediation($ip)); } catch (\Exception $e) { - throw new BouncerException($e->getMessage(), (int)$e->getCode(), $e); + throw new BouncerException($e->getMessage(), (int) $e->getCode(), $e); } } /** - * Get the current IP, even if it's the IP of a proxy + * Get the current IP, even if it's the IP of a proxy. */ abstract public function getRemoteIp(): string; /** - * Get current request uri + * Get current request uri. */ abstract public function getRequestUri(): string; @@ -196,7 +192,7 @@ public function pruneCache(): bool try { return $this->getRemediationEngine()->pruneCache(); } catch (\Exception $e) { - throw new BouncerException('Error while pruning cache: ' . $e->getMessage(), (int)$e->getCode(), $e); + throw new BouncerException('Error while pruning cache: ' . $e->getMessage(), (int) $e->getCode(), $e); } } @@ -213,14 +209,14 @@ public function refreshBlocklistCache(): array try { return $this->getRemediationEngine()->refreshDecisions(); } catch (\Exception $e) { - throw new BouncerException('Error while refreshing decisions: ' . $e->getMessage(), (int)$e->getCode(), $e); + $message = 'Error while refreshing decisions: ' . $e->getMessage(); + throw new BouncerException($message, (int) $e->getCode(), $e); } } /** - * Handle a bounce for current IP + * Handle a bounce for current IP. * - * @return bool * @throws BouncerException * @throws CacheException * @throws InvalidArgumentException @@ -243,7 +239,7 @@ public function run(): bool 'line' => $e->getLine(), ]); if (true === $this->getConfig('display_errors')) { - throw new BouncerException($e->getMessage(), (int)$e->getCode(), $e); + throw new BouncerException($e->getMessage(), (int) $e->getCode(), $e); } } @@ -279,9 +275,8 @@ public function shouldBounceCurrentIp(): bool } /** - * Process a simple cache test + * Process a simple cache test. * - * @return void * @throws BouncerException * @throws InvalidArgumentException */ @@ -291,11 +286,8 @@ public function testCacheConnection(): void $cache = $this->getRemediationEngine()->getCacheStorage(); $cache->getItem(AbstractCache::CONFIG); } catch (\Exception $e) { - throw new BouncerException( - 'Error while testing cache connection: ' . $e->getMessage(), - (int)$e->getCode(), - $e - ); + $message = 'Error while testing cache connection: ' . $e->getMessage(); + throw new BouncerException($message, (int) $e->getCode(), $e); } } @@ -316,10 +308,8 @@ protected function handleCache(array $configs, LoggerInterface $logger): Abstrac case Constants::CACHE_SYSTEM_REDIS: $cache = new Redis($configs, $logger); break; - // @codeCoverageIgnoreStart default: throw new BouncerException("Unknown selected cache technology: $cacheSystem"); - // @codeCoverageIgnoreEnd } return $cache; @@ -335,14 +325,10 @@ protected function handleClient(array $configs, LoggerInterface $logger): Bounce /** * Handle remediation for some IP. * - * @param string $remediation - * @param string $ip - * @return void * @throws CacheException * @throws InvalidArgumentException * @throws \Symfony\Component\Cache\Exception\InvalidArgumentException * @throws BouncerException - * */ protected function handleRemediation(string $remediation, string $ip): void { @@ -362,8 +348,6 @@ protected function handleRemediation(string $remediation, string $ip): void } /** - * @param string $redirect - * @return void * @codeCoverageIgnore */ protected function redirectResponse(string $redirect): void @@ -374,9 +358,10 @@ protected function redirectResponse(string $redirect): void /** * Send HTTP response. + * * @throws BouncerException - * @codeCoverageIgnore * + * @codeCoverageIgnore */ protected function sendResponse(string $body, int $statusCode): void { @@ -406,7 +391,7 @@ protected function sendResponse(string $body, int $statusCode): void * Build a captcha couple. * * @return array an array composed of two items, a "phrase" string representing the phrase and a "inlineImage" - * representing the image data + * representing the image data */ private function buildCaptchaCouple(): array { @@ -444,8 +429,8 @@ private function capRemediationLevel(string $remediation): string break; } - $currentIndex = (int)array_search($remediation, $orderedRemediations); - $maxIndex = (int)array_search( + $currentIndex = (int) array_search($remediation, $orderedRemediations); + $maxIndex = (int) array_search( $maxRemediationLevel, $orderedRemediations ); @@ -454,7 +439,7 @@ private function capRemediationLevel(string $remediation): string $finalRemediation = $orderedRemediations[$maxIndex]; $this->logger->debug('Original remediation has been capped', [ 'origin' => $remediation, - 'final' => $finalRemediation + 'final' => $finalRemediation, ]); } $this->logger->info('Final remediation', [ @@ -471,8 +456,8 @@ private function capRemediationLevel(string $remediation): string * - (0 is interpreted as "o" and 1 in interpreted as "l"). * * @param string $expected The expected phrase - * @param string $try The phrase to check (the user input) - * @param string $ip The IP of the use (for logging purpose) + * @param string $try The phrase to check (the user input) + * @param string $ip The IP of the use (for logging purpose) * * @return bool If the captcha input was correct or not * @@ -494,7 +479,6 @@ private function checkCaptcha(string $expected, string $try, string $ip): bool * Configure this instance. * * @param array $config An array with all configuration parameters - * */ private function configure(array $config): void { @@ -505,12 +489,10 @@ private function configure(array $config): void } /** - * @param string $ip - * @return void * @throws InvalidArgumentException * @throws BouncerException - * @codeCoverageIgnore * + * @codeCoverageIgnore */ private function displayCaptchaWall(string $ip): void { @@ -520,8 +502,8 @@ private function displayCaptchaWall(string $ip): void $ip ); $body = $this->getCaptchaHtml( - (bool)$captchaVariables['resolution_failed'], - (string)$captchaVariables['inline_image'], + (bool) $captchaVariables['resolution_failed'], + (string) $captchaVariables['inline_image'], '' ); $this->sendResponse($body, 401); @@ -531,7 +513,6 @@ private function displayCaptchaWall(string $ip): void * Returns a default "CrowdSec 403" HTML template. * The input $config should match the TemplateConfiguration input format. * - * * @return string The HTML compiled template */ private function getBanHtml(): string @@ -548,11 +529,6 @@ private function getCache(): AbstractCache /** * Returns a default "CrowdSec Captcha (401)" HTML template. - * - * @param bool $error - * @param string $captchaImageSrc - * @param string $captchaResolutionFormUrl - * @return string */ private function getCaptchaHtml( bool $error, @@ -566,7 +542,7 @@ private function getCaptchaHtml( [ 'error' => $error, 'captcha_img' => $captchaImageSrc, - 'captcha_resolution_url' => $captchaResolutionFormUrl + 'captcha_resolution_url' => $captchaResolutionFormUrl, ] )); } @@ -580,12 +556,10 @@ private function getTrustForwardedIpBoundsList(): array } /** - * @return void - * * @throws BouncerException * @throws BouncerException - * @codeCoverageIgnore * + * @codeCoverageIgnore */ private function handleBanRemediation(): void { @@ -594,15 +568,10 @@ private function handleBanRemediation(): void } /** - * @param string $ip - * - * @return void - * * @throws BouncerException * @throws CacheException * @throws InvalidArgumentException * @throws \Symfony\Component\Cache\Exception\InvalidArgumentException - * */ private function handleCaptchaRemediation(string $ip): void { @@ -636,14 +605,11 @@ private function handleCaptchaRemediation(string $ip): void } /** - * @param string $ip - * @return void - * * @throws CacheException * @throws InvalidArgumentException * @throws \Symfony\Component\Cache\Exception\InvalidArgumentException - * @SuppressWarnings(PHPMD.ElseExpression) * + * @SuppressWarnings(PHPMD.ElseExpression) */ private function handleCaptchaResolutionForm(string $ip): void { @@ -668,8 +634,8 @@ private function handleCaptchaResolutionForm(string $ip): void $duration = $this->getConfig('captcha_cache_duration') ?? Constants::CACHE_EXPIRATION_FOR_CAPTCHA; if ( $this->checkCaptcha( - (string)$cachedCaptchaVariables['phrase_to_guess'], - (string)$this->getPostedVariable('phrase'), + (string) $cachedCaptchaVariables['phrase_to_guess'], + (string) $this->getPostedVariable('phrase'), $ip ) ) { @@ -710,11 +676,7 @@ private function handleCaptchaResolutionForm(string $ip): void } /** - * Handle X-Forwarded-For HTTP header to retrieve the IP to bounce - * - * @param string $ip - * @param array $configs - * @return string + * Handle X-Forwarded-For HTTP header to retrieve the IP to bounce. * * @SuppressWarnings(PHPMD.ElseExpression) */ @@ -727,13 +689,13 @@ private function handleForwardedFor(string $ip, array $configs): string $ipList = array_map('trim', array_values(array_filter(explode(',', $xForwardedForHeader)))); $forwardedIp = end($ipList); } - } elseif ($configs['forced_test_forwarded_ip'] === Constants::X_FORWARDED_DISABLED) { + } elseif (Constants::X_FORWARDED_DISABLED === $configs['forced_test_forwarded_ip']) { $this->logger->debug('X-Forwarded-for usage is disabled', [ 'type' => 'DISABLED_X_FORWARDED_FOR_USAGE', 'original_ip' => $ip, ]); } else { - $forwardedIp = (string)$configs['forced_test_forwarded_ip']; + $forwardedIp = (string) $configs['forced_test_forwarded_ip']; $this->logger->debug('X-Forwarded-for usage is forced', [ 'type' => 'FORCED_X_FORWARDED_FOR_USAGE', 'original_ip' => $ip, @@ -765,7 +727,6 @@ private function handleForwardedFor(string $ip, array $configs): string * @throws CacheException * @throws InvalidArgumentException * @throws \Symfony\Component\Cache\Exception\InvalidArgumentException - * */ private function initCaptchaResolution(string $ip): void { @@ -793,11 +754,10 @@ private function initCaptchaResolution(string $ip): void * @throws CacheException * @throws InvalidArgumentException * @throws \Symfony\Component\Cache\Exception\InvalidArgumentException - * */ private function refreshCaptcha(string $ip): bool { - if (null !== $this->getPostedVariable('refresh') && (int)$this->getPostedVariable('refresh')) { + if (null !== $this->getPostedVariable('refresh') && (int) $this->getPostedVariable('refresh')) { // Generate new captcha image for the user $captchaCouple = $this->buildCaptchaCouple(); $captchaVariables = [ @@ -821,16 +781,12 @@ private function refreshCaptcha(string $ip): bool } /** - * Check - * - * @param array $cachedCaptchaVariables - * @return bool - * + * Check if captcha resolution is required or not. */ - private function shouldNotCheckResolution(array $cachedCaptchaVariables): bool + private function shouldNotCheckResolution(array $captchaData): bool { $result = false; - if (\in_array($cachedCaptchaVariables['has_to_be_resolved'], [null, false])) { + if (\in_array($captchaData['has_to_be_resolved'], [null, false])) { // Check not needed if 'has_to_be_resolved' cached flag has not been saved $result = true; } elseif ('POST' !== $this->getHttpMethod() || null === $this->getPostedVariable('crowdsec_captcha')) { diff --git a/src/Configuration.php b/src/Configuration.php index 8dd8667..27c9c93 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -5,7 +5,6 @@ namespace CrowdSecBouncer; use CrowdSec\Common\Configuration\AbstractConfiguration; -use InvalidArgumentException; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; @@ -46,7 +45,8 @@ class Configuration extends AbstractConfiguration /** * {@inheritdoc} - * @throws InvalidArgumentException + * + * @throws \InvalidArgumentException */ public function getConfigTreeBuilder(): TreeBuilder { @@ -63,11 +63,13 @@ public function getConfigTreeBuilder(): TreeBuilder } /** - * Bouncer settings + * Bouncer settings. * * @param NodeDefinition|ArrayNodeDefinition $rootNode + * * @return void - * @throws InvalidArgumentException + * + * @throws \InvalidArgumentException */ private function addBouncerNodes($rootNode) { @@ -77,7 +79,7 @@ private function addBouncerNodes($rootNode) [ Constants::BOUNCING_LEVEL_DISABLED, Constants::BOUNCING_LEVEL_NORMAL, - Constants::BOUNCING_LEVEL_FLEX + Constants::BOUNCING_LEVEL_FLEX, ] ) ->defaultValue(Constants::BOUNCING_LEVEL_NORMAL) @@ -94,11 +96,13 @@ private function addBouncerNodes($rootNode) } /** - * Cache settings + * Cache settings. * * @param NodeDefinition|ArrayNodeDefinition $rootNode + * * @return void - * @throws InvalidArgumentException + * + * @throws \InvalidArgumentException */ private function addCacheNodes($rootNode) { @@ -108,7 +112,7 @@ private function addCacheNodes($rootNode) [ Constants::CACHE_SYSTEM_PHPFS, Constants::CACHE_SYSTEM_REDIS, - Constants::CACHE_SYSTEM_MEMCACHED + Constants::CACHE_SYSTEM_MEMCACHED, ] ) ->defaultValue(Constants::CACHE_SYSTEM_PHPFS) @@ -120,9 +124,10 @@ private function addCacheNodes($rootNode) } /** - * LAPI connection settings + * LAPI connection settings. * * @param NodeDefinition|ArrayNodeDefinition $rootNode + * * @return void */ private function addConnectionNodes($rootNode) @@ -133,9 +138,10 @@ private function addConnectionNodes($rootNode) } /** - * Debug settings + * Debug settings. * * @param NodeDefinition|ArrayNodeDefinition $rootNode + * * @return void */ private function addDebugNodes($rootNode) @@ -150,6 +156,10 @@ private function addDebugNodes($rootNode) ->end(); } + /** + * @param $rootNode + * @return void + */ private function addTemplateNodes($rootNode) { $defaultSubtitle = 'This page is protected against cyber attacks and your IP has been banned by our system.'; diff --git a/src/Constants.php b/src/Constants.php index 3215a50..3580245 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -37,9 +37,9 @@ class Constants extends RemConstants /** @var string The Default URL of the CrowdSec LAPI */ public const DEFAULT_LAPI_URL = 'http://localhost:8080'; /** @var string Path for html templates folder (e.g. ban and captcha wall) */ - public const TEMPLATES_DIR = __DIR__ . "/templates"; + public const TEMPLATES_DIR = __DIR__ . '/templates'; /** @var string The last version of this library */ - public const VERSION = 'v1.4.0'; + public const VERSION = 'v2.0.0'; /** @var string The "disabled" x-forwarded-for setting */ public const X_FORWARDED_DISABLED = 'no_forward'; } diff --git a/src/Fixes/Gregwar/Captcha/CaptchaBuilder.php b/src/Fixes/Gregwar/Captcha/CaptchaBuilder.php index aaf0d8e..5cd8418 100644 --- a/src/Fixes/Gregwar/Captcha/CaptchaBuilder.php +++ b/src/Fixes/Gregwar/Captcha/CaptchaBuilder.php @@ -6,23 +6,24 @@ /** * Override to : - * - fix "implicit conversion error on PHP 8.1" + * - fix "implicit conversion error on PHP 8.1". * * @see https://github.com/crowdsecurity/php-cs-bouncer/issues/62 and * @see https://github.com/Gregwar/Captcha/pull/101/files + * * @SuppressWarnings(PHPMD.ElseExpression) - * @codeCoverageIgnore * + * @codeCoverageIgnore */ class CaptchaBuilder extends GregwarCaptchaBuilder { /** - * Writes the phrase on the image + * Writes the phrase on the image. */ protected function writePhrase($image, $phrase, $font, $width, $height) { $length = mb_strlen($phrase); - if ($length === 0) { + if (0 === $length) { return \imagecolorallocate($image, 0, 0, 0); } @@ -35,14 +36,14 @@ protected function writePhrase($image, $phrase, $font, $width, $height) $y = (int) round(($height - $textHeight) / 2) + $size; if (!$this->textColor) { - $textColor = array($this->rand(0, 150), $this->rand(0, 150), $this->rand(0, 150)); + $textColor = [$this->rand(0, 150), $this->rand(0, 150), $this->rand(0, 150)]; } else { $textColor = $this->textColor; } $col = \imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]); // Write the letters one by one, with random angle - for ($i = 0; $i < $length; $i++) { + for ($i = 0; $i < $length; ++$i) { $symbol = mb_substr($phrase, $i, 1); $box = \imagettfbbox($size, 0, $font, $symbol); $w = $box[2] - $box[0]; diff --git a/src/StandaloneBouncer.php b/src/StandaloneBouncer.php deleted file mode 100644 index 1f6706b..0000000 --- a/src/StandaloneBouncer.php +++ /dev/null @@ -1,121 +0,0 @@ - true]); - $this->logger = $logger ?: new FileLog($logConfigs, 'php_standalone_bouncer'); - $configs = $this->handleTrustedIpsConfig($configs); - $configs['user_agent_version'] = Constants::VERSION; - $configs['user_agent_suffix'] = 'Standalone'; - $client = $this->handleClient($configs, $this->logger); - $cache = $this->handleCache($configs, $this->logger); - $remediation = new LapiRemediation($configs, $client, $cache, $this->logger); - - parent::__construct($configs, $remediation, $this->logger); - } - - /** - * The current HTTP method. - */ - public function getHttpMethod(): string - { - return $_SERVER['REQUEST_METHOD'] ?? ""; - } - - /** - * @param string $name Ex: "X-Forwarded-For" - */ - public function getHttpRequestHeader(string $name): ?string - { - $headerName = 'HTTP_' . str_replace('-', '_', strtoupper($name)); - if (!\array_key_exists($headerName, $_SERVER)) { - return null; - } - - return is_string($_SERVER[$headerName]) ? $_SERVER[$headerName] : null; - } - - /** - * Get the value of a posted field. - */ - public function getPostedVariable(string $name): ?string - { - if (!isset($_POST[$name])) { - return null; - } - - return is_string($_POST[$name]) ? $_POST[$name] : null; - } - - /** - * @return string The current IP, even if it's the IP of a proxy - */ - public function getRemoteIp(): string - { - return $_SERVER['REMOTE_ADDR'] ?? ""; - } - - /** - * The current URI. - */ - public function getRequestUri(): string - { - return $_SERVER['REQUEST_URI'] ?? ""; - } - - /** - * The Standalone bouncer "trust_ip_forward_array" config accepts an array of IPs. - * This method will return array of comparable IPs array - * - * @param array $configs // ['1.2.3.4'] - * @return array // [['001.002.003.004', '001.002.003.004']] - * @throws BouncerException - */ - private function handleTrustedIpsConfig(array $configs): array - { - // Convert array of string to array of array with comparable IPs - if (isset($configs['trust_ip_forward_array']) && \is_array(($configs['trust_ip_forward_array']))) { - $forwardConfigs = $configs['trust_ip_forward_array']; - $finalForwardConfigs = []; - foreach ($forwardConfigs as $forwardConfig) { - if (!\is_string($forwardConfig)) { - throw new BouncerException("'trust_ip_forward_array' config must be an array of string"); - } - $parsedString = Factory::parseAddressString($forwardConfig, 3); - if (!empty($parsedString)) { - $comparableValue = $parsedString->getComparableString(); - $finalForwardConfigs[] = [$comparableValue, $comparableValue]; - } - } - $configs['trust_ip_forward_array'] = $finalForwardConfigs; - } - - return $configs; - } -} diff --git a/src/Template.php b/src/Template.php index 154d22d..b7caf2c 100644 --- a/src/Template.php +++ b/src/Template.php @@ -4,11 +4,11 @@ namespace CrowdSecBouncer; +use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\Loader\FilesystemLoader; -use Twig\Environment; use Twig\TemplateWrapper; /** @@ -27,9 +27,6 @@ class Template private $template; /** - * @param string $path - * @param string $templatesDir - * @param array $options * @throws LoaderError * @throws RuntimeError * @throws SyntaxError diff --git a/tests/Integration/IpVerificationTest.php b/tests/Integration/AbstractBouncerTest.php similarity index 57% rename from tests/Integration/IpVerificationTest.php rename to tests/Integration/AbstractBouncerTest.php index f29f256..31f00b8 100644 --- a/tests/Integration/IpVerificationTest.php +++ b/tests/Integration/AbstractBouncerTest.php @@ -4,19 +4,24 @@ namespace CrowdSecBouncer\Tests\Integration; +use CrowdSec\Common\Client\RequestHandler\Curl; +use CrowdSec\Common\Client\RequestHandler\FileGetContents; use CrowdSec\Common\Logger\FileLog; -use CrowdSec\RemediationEngine\CacheStorage\CacheStorageException; +use CrowdSec\LapiClient\Bouncer as BouncerClient; +use CrowdSec\RemediationEngine\CacheStorage\Memcached; +use CrowdSec\RemediationEngine\CacheStorage\PhpFiles; +use CrowdSec\RemediationEngine\CacheStorage\Redis; +use CrowdSec\RemediationEngine\LapiRemediation; +use CrowdSecBouncer\AbstractBouncer; use CrowdSecBouncer\Bouncer; use CrowdSecBouncer\BouncerException; use CrowdSecBouncer\Constants; -use CrowdSecBouncer\StandaloneBouncer; use CrowdSecBouncer\Tests\PHPUnitUtil; use org\bovigo\vfs\vfsStream; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; /** - * @covers \CrowdSecBouncer\StandaloneBouncer::getRemediationForIp * @covers \CrowdSecBouncer\AbstractBouncer::clearCache * @covers \CrowdSecBouncer\AbstractBouncer::pruneCache * @covers \CrowdSecBouncer\AbstractBouncer::testCacheConnection @@ -28,9 +33,11 @@ * @uses \CrowdSecBouncer\AbstractBouncer::getConfigs * @uses \CrowdSecBouncer\AbstractBouncer::getLogger * @uses \CrowdSecBouncer\AbstractBouncer::getRemediationEngine + * * @covers \CrowdSecBouncer\AbstractBouncer::handleCache - * @uses \CrowdSecBouncer\AbstractBouncer::handleClient + * @covers \CrowdSecBouncer\AbstractBouncer::handleClient * @covers \CrowdSecBouncer\AbstractBouncer::refreshBlocklistCache + * * @uses \CrowdSecBouncer\Configuration::addBouncerNodes * @uses \CrowdSecBouncer\Configuration::addCacheNodes * @uses \CrowdSecBouncer\Configuration::addConnectionNodes @@ -38,53 +45,41 @@ * @uses \CrowdSecBouncer\Configuration::addTemplateNodes * @uses \CrowdSecBouncer\Configuration::cleanConfigs * @uses \CrowdSecBouncer\Configuration::getConfigTreeBuilder - * @uses \CrowdSecBouncer\StandaloneBouncer::__construct - * @uses \CrowdSecBouncer\StandaloneBouncer::handleTrustedIpsConfig * * @covers \CrowdSecBouncer\AbstractBouncer::bounceCurrentIp * @covers \CrowdSecBouncer\AbstractBouncer::getTrustForwardedIpBoundsList * @covers \CrowdSecBouncer\AbstractBouncer::handleForwardedFor * @covers \CrowdSecBouncer\AbstractBouncer::handleRemediation * @covers \CrowdSecBouncer\AbstractBouncer::shouldTrustXforwardedFor - * @covers \CrowdSecBouncer\StandaloneBouncer::getHttpRequestHeader - * @covers \CrowdSecBouncer\StandaloneBouncer::getRemoteIp - * @covers \CrowdSecBouncer\StandaloneBouncer::run - * @covers \CrowdSecBouncer\StandaloneBouncer::shouldBounceCurrentIp - * @covers \CrowdSecBouncer\StandaloneBouncer::getHttpMethod - * @covers \CrowdSecBouncer\StandaloneBouncer::getPostedVariable - * * * @uses \CrowdSecBouncer\AbstractBouncer::getBanHtml * @uses \CrowdSecBouncer\AbstractBouncer::handleBanRemediation * @uses \CrowdSecBouncer\AbstractBouncer::sendResponse * @uses \CrowdSecBouncer\Template::__construct * @uses \CrowdSecBouncer\Template::render - * * @uses \CrowdSecBouncer\AbstractBouncer::buildCaptchaCouple * @uses \CrowdSecBouncer\AbstractBouncer::displayCaptchaWall + * * @covers \CrowdSecBouncer\AbstractBouncer::getCache + * * @uses \CrowdSecBouncer\AbstractBouncer::getCaptchaHtml + * * @covers \CrowdSecBouncer\AbstractBouncer::handleCaptchaRemediation * @covers \CrowdSecBouncer\AbstractBouncer::handleCaptchaResolutionForm * @covers \CrowdSecBouncer\AbstractBouncer::initCaptchaResolution * @covers \CrowdSecBouncer\AbstractBouncer::shouldNotCheckResolution * @covers \CrowdSecBouncer\AbstractBouncer::checkCaptcha * @covers \CrowdSecBouncer\AbstractBouncer::refreshCaptcha - * @covers \CrowdSecBouncer\StandaloneBouncer::getRequestUri - * - * - * + * @covers \CrowdSecBouncer\AbstractBouncer::getRemediationForIp + * @covers \CrowdSecBouncer\AbstractBouncer::run + * @covers \CrowdSecBouncer\AbstractBouncer::shouldBounceCurrentIp */ -final class IpVerificationTest extends TestCase +final class AbstractBouncerTest extends TestCase { - private const EXCLUDED_URI = '/favicon.ico'; /** @var WatcherClient */ private $watcherClient; - /** @var bool */ - private $useCurl; - /** @var bool */ private $useTls; /** @@ -111,8 +106,7 @@ final class IpVerificationTest extends TestCase protected function setUp(): void { - $this->useTls = (string)getenv('BOUNCER_TLS_PATH'); - $this->useCurl = (bool)getenv('USE_CURL'); + $this->useTls = (string) getenv('BOUNCER_TLS_PATH'); $this->root = vfsStream::setup('/tmp'); $this->configs['log_directory_path'] = $this->root->url(); @@ -126,14 +120,13 @@ protected function setUp(): void 'auth_type' => $this->useTls ? \CrowdSec\LapiClient\Constants::AUTH_TLS : Constants::AUTH_KEY, 'api_key' => getenv('BOUNCER_KEY'), 'api_url' => getenv('LAPI_URL'), - 'use_curl' => $this->useCurl, 'user_agent_suffix' => 'testphpbouncer', 'fs_cache_path' => $this->root->url() . '/.cache', 'redis_dsn' => getenv('REDIS_DSN'), 'memcached_dsn' => getenv('MEMCACHED_DSN'), 'excluded_uris' => [self::EXCLUDED_URI], 'stream_mode' => false, - 'trust_ip_forward_array' => ['5.6.7.8'] + 'trust_ip_forward_array' => [['005.006.007.008', '005.006.007.008']], ]; if ($this->useTls) { $this->addTlsConfig($bouncerConfigs, $this->useTls); @@ -145,40 +138,6 @@ protected function setUp(): void $this->watcherClient->deleteAllDecisions(); } - public function cacheAdapterConfigProvider(): array - { - return TestHelpers::cacheAdapterConfigProvider(); - } - - private function cacheAdapterCheck($cacheAdapter, $origCacheName) - { - switch ($origCacheName) { - case 'PhpFilesAdapter': - $this->assertEquals( - 'CrowdSec\RemediationEngine\CacheStorage\PhpFiles', - get_class($cacheAdapter), - 'Tested adapter should be correct' - ); - break; - case 'MemcachedAdapter': - $this->assertEquals( - 'CrowdSec\RemediationEngine\CacheStorage\Memcached', - get_class($cacheAdapter), - 'Tested adapter should be correct' - ); - break; - case 'RedisAdapter': - $this->assertEquals( - 'CrowdSec\RemediationEngine\CacheStorage\Redis', - get_class($cacheAdapter), - 'Tested adapter should be correct' - ); - break; - default: - break; - } - } - private function addTlsConfig(&$bouncerConfigs, $tlsPath) { $bouncerConfigs['tls_cert_path'] = $tlsPath . '/bouncer.pem'; @@ -187,73 +146,14 @@ private function addTlsConfig(&$bouncerConfigs, $tlsPath) $bouncerConfigs['tls_verify_peer'] = true; } - /** - * @group integration - * @dataProvider cacheAdapterConfigProvider - */ - public function testTestCacheConnexion($cacheAdapterName, $origCacheName) - { - $bouncer = new StandaloneBouncer(array_merge($this->configs, - ['cache_system'=> $cacheAdapterName])); - $error = ''; - try { - $bouncer->testCacheConnection(); - } catch (\Exception $e){ - $error = $e->getMessage(); - } - $this->assertEquals('', $error); - - // Test custom error handler for Memcached - if($cacheAdapterName === 'memcached'){ - $configs = array_merge($this->configs, - [ - 'cache_system'=> $cacheAdapterName, - 'memcached_dsn' => 'memcached://memcached:21', - ]); - $bouncer2 = new StandaloneBouncer($configs); - - $error = ''; - try { - $bouncer2->testCacheConnection(); - } catch (BouncerException $e){ - $error = $e->getMessage(); - } - PHPUnitUtil::assertRegExp( - $this, - '/Error while testing cache connection/', - $error, - 'Should have throw an error' - ); - } - // Test bad dsn for redis - if($cacheAdapterName === 'redis'){ - $error = ''; - try { - $bouncer3 = new StandaloneBouncer(array_merge($this->configs, - [ - 'cache_system'=> $cacheAdapterName, - 'redis_dsn' => 'redis://redis:21' - ])); - } catch (CacheStorageException $e){ - $error = $e->getMessage(); - } - PHPUnitUtil::assertRegExp( - $this, - '/Error when creating/', - $error, - 'Should have throw an error' - ); - - } - } - public function testConstructAndSomeMethods() { - unset($_SERVER['REMOTE_ADDR'] ); - $bouncer = new StandaloneBouncer(array_merge($this->configs, ['unexpected_config' => 'test'])); - $this->assertEquals('', $bouncer->getRemoteIp(), 'Should return empty string'); - $_SERVER['REMOTE_ADDR'] = '5.6.7.8'; - $this->assertEquals('5.6.7.8', $bouncer->getRemoteIp(), 'Should remote ip'); + $bouncerConfigs = array_merge($this->configs, ['unexpected_config' => 'test']); + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + $logger = new FileLog(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation, $logger]); $this->assertEquals(false, $bouncer->getConfig('stream_mode'), 'Stream mode config'); $this->assertEquals(FileLog::class, \get_class($bouncer->getLogger()), 'Logger Init'); @@ -264,406 +164,245 @@ public function testConstructAndSomeMethods() $this->assertEquals('CrowdSec\RemediationEngine\LapiRemediation', \get_class($remediation), 'Remediation Init'); $this->assertEquals('CrowdSec\RemediationEngine\CacheStorage\PhpFiles', \get_class($remediation->getCacheStorage()), 'Remediation cache Init'); - $error = ''; - - try { - $bouncer = - new StandaloneBouncer(array_merge($this->configs, ['trust_ip_forward_array' => [['001.002.003.004', '001.002.003.004']]])); - } catch (BouncerException $e) { - $error = $e->getMessage(); - } - - PHPUnitUtil::assertRegExp( - $this, - '/\'trust_ip_forward_array\' config must be an array of string/', - $error, - 'Should have throw an error' - ); - $this->assertEquals([['005.006.007.008', '005.006.007.008']], $bouncer->getConfig('trust_ip_forward_array'), 'Forwarded array config'); $this->assertEquals(Constants::BOUNCING_LEVEL_NORMAL, $bouncer->getConfig('bouncing_level'), 'Bouncing level config'); $this->assertEquals(null, $bouncer->getConfig('unexpected_config'), 'Should clean config'); - $_SERVER['REQUEST_METHOD'] = 'POST'; - $this->assertEquals('POST', $bouncer->getHttpMethod(), 'Should get HTTP method'); - - $this->assertEquals(null, $bouncer->getHttpRequestHeader('X-Forwarded-For'), 'Should get HTTP header'); - $_SERVER['HTTP_X_FORWARDED_FOR'] = '1.2.3.5'; - $this->assertEquals('1.2.3.5', $bouncer->getHttpRequestHeader('X-Forwarded-For'), 'Should get HTTP header'); - - $_POST['test'] = 'test-post'; - $this->assertEquals(null, $bouncer->getPostedVariable('hello'), 'Should return null for non Posted variable'); - $this->assertEquals('test-post', $bouncer->getPostedVariable('test'), 'Should get Posted variable'); - $configs = $bouncer->getConfigs(); $this->assertArrayHasKey('text', $configs, 'Config should have text key'); $this->assertArrayHasKey('color', $configs, 'Config should have color key'); - - $this->configs['cache_system'] = Constants::CACHE_SYSTEM_REDIS; - $bouncer = new StandaloneBouncer($this->configs); - - $remediation = $bouncer->getRemediationEngine(); - $this->assertEquals('CrowdSec\RemediationEngine\CacheStorage\Redis', \get_class($remediation->getCacheStorage - ()), 'Remediation cache Init'); - - $this->configs['cache_system'] = Constants::CACHE_SYSTEM_MEMCACHED; - $bouncer = new StandaloneBouncer($this->configs); - - $remediation = $bouncer->getRemediationEngine(); - $this->assertEquals('CrowdSec\RemediationEngine\CacheStorage\Memcached', \get_class - ($remediation->getCacheStorage - ()), 'Remediation cache Init'); } /** - * @group integration - * @dataProvider cacheAdapterConfigProvider + * @group captcha + * + * @return void + * + * @throws BouncerException + * @throws \CrowdSec\RemediationEngine\CacheStorage\CacheStorageException + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException */ - public function testCanVerifyIpInLiveModeWithCacheSystem($cacheAdapterName, $origCacheName): void + public function testCaptchaFlow() { - // Init context - $this->watcherClient->setInitialState(); - + $this->watcherClient->setSimpleDecision('captcha'); // Init bouncer $bouncerConfigs = [ 'auth_type' => $this->useTls ? Constants::AUTH_TLS : Constants::AUTH_KEY, 'api_key' => TestHelpers::getBouncerKey(), 'api_url' => TestHelpers::getLapiUrl(), - 'use_curl' => $this->useCurl, - 'cache_system' => $cacheAdapterName, - 'redis_dsn' => getenv('REDIS_DSN'), - 'memcached_dsn' => getenv('MEMCACHED_DSN'), + 'stream_mode' => false, + 'cache_system' => Constants::CACHE_SYSTEM_PHPFS, 'fs_cache_path' => $this->root->url() . '/.cache', - 'stream_mode' => false + 'forced_test_ip' => TestHelpers::BAD_IP, ]; if ($this->useTls) { $this->addTlsConfig($bouncerConfigs, $this->useTls); } - $bouncer = new StandaloneBouncer($bouncerConfigs, $this->logger); + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); - // Test cache adapter - $cacheAdapter = $bouncer->getRemediationEngine()->getCacheStorage(); - $cacheAdapter->clear(); - $this->cacheAdapterCheck($cacheAdapter, $origCacheName); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation], '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + ]); - $this->assertEquals( - 'ban', - $bouncer->getRemediationForIp(TestHelpers::BAD_IP), - 'Get decisions for a bad IP (for the first time, it should be a cache miss)' - ); + $bouncer->clearCache(); + $cache = $bouncer->getRemediationEngine()->getCacheStorage(); + $cacheKey = $cache->getCacheKey(Constants::SCOPE_IP, TestHelpers::BAD_IP); + $item = $cache->getItem($cacheKey); $this->assertEquals( - 'ban', - $bouncer->getRemediationForIp(TestHelpers::BAD_IP), - 'Call the same thing for the second time (now it should be a cache hit)' + false, + $item->isHit(), + 'The remediation should not be cached' ); - $cleanRemediation1stCall = $bouncer->getRemediationForIp(TestHelpers::CLEAN_IP); + $cacheKeyCaptcha = $cache->getCacheKey(Constants::CACHE_TAG_CAPTCHA, TestHelpers::BAD_IP); + $item = $cache->getItem($cacheKeyCaptcha); $this->assertEquals( - 'bypass', - $cleanRemediation1stCall, - 'Get decisions for a clean IP for the first time (it should be a cache miss)' + false, + $item->isHit(), + 'The captcha variables should not be cached' ); - // Call the same thing for the second time (now it should be a cache hit) - $cleanRemediation2ndCall = $bouncer->getRemediationForIp(TestHelpers::CLEAN_IP); - $this->assertEquals('bypass', $cleanRemediation2ndCall); - - // Prune cache - if ('PhpFilesAdapter' === $origCacheName) { - $this->assertTrue($bouncer->pruneCache(), 'The cache should be prunable'); - } - - // Clear cache - $this->assertTrue($bouncer->clearCache(), 'The cache should be clearable'); - - // Call one more time (should miss as the cache has been cleared) - - $remediation3rdCall = $bouncer->getRemediationForIp(TestHelpers::BAD_IP); - $this->assertEquals('ban', $remediation3rdCall); - - // Reconfigure the bouncer to set maximum remediation level to "captcha" - $bouncerConfigs['bouncing_level'] = Constants::BOUNCING_LEVEL_FLEX; - $bouncer = new StandaloneBouncer($bouncerConfigs, $this->logger); - $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IP); - $this->assertEquals('captcha', $cappedRemediation, 'The remediation for the banned IP should now be "captcha"'); - // Reset the max remediation level to its origin state - $bouncerConfigs['bouncing_level'] = Constants::BOUNCING_LEVEL_NORMAL; - $bouncer = new StandaloneBouncer($bouncerConfigs, $this->logger); + // Step 1 : access a page should display a captcha wall + $bouncer->bounceCurrentIp(); - $this->logger->info('', ['message' => 'set "Large IPV4 range banned" state']); - $this->watcherClient->deleteAllDecisions(); - $this->watcherClient->addDecision( - new \DateTime(), - '24h', - WatcherClient::HOURS24, - TestHelpers::BAD_IP . '/' . TestHelpers::LARGE_IPV4_RANGE, - 'ban' - ); - $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IP); + $item = $cache->getItem($cacheKey); $this->assertEquals( - 'ban', - $cappedRemediation, - 'The remediation for the banned IPv4 range should be ban' + true, + $item->isHit(), + 'The remediation should be cached' ); - $this->logger->info('', ['message' => 'set "IPV6 range banned" state']); - $this->watcherClient->deleteAllDecisions(); - $this->watcherClient->addDecision( - new \DateTime(), - '24h', - WatcherClient::HOURS24, - TestHelpers::BAD_IPV6 . '/' . TestHelpers::IPV6_RANGE, - 'ban' - ); - $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IPV6); - $this->assertEquals( - 'ban', - $cappedRemediation, - 'The remediation for a banned IPv6 range should be ban in live mode' - ); - $this->watcherClient->deleteAllDecisions(); - $this->watcherClient->addDecision( - new \DateTime(), - '24h', - WatcherClient::HOURS24, - TestHelpers::BAD_IPV6, - 'ban' - ); - $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IPV6); + $cacheKeyCaptcha = $cache->getCacheKey(Constants::CACHE_TAG_CAPTCHA, TestHelpers::BAD_IP); + $item = $cache->getItem($cacheKeyCaptcha); $this->assertEquals( - 'ban', - $cappedRemediation, - 'The remediation for a banned IPv6 should be ban' + true, + $item->isHit(), + 'The captcha variables should be cached' ); - } - - /** - * @group integration - * @dataProvider cacheAdapterConfigProvider - */ - public function testCanVerifyIpInStreamModeWithCacheSystem($cacheAdapterName, $origCacheName): void - { - // Init context - $this->watcherClient->setInitialState(); - // Init bouncer - $bouncerConfigs = [ - 'auth_type' => $this->useTls ? Constants::AUTH_TLS : Constants::AUTH_KEY, - 'api_key' => TestHelpers::getBouncerKey(), - 'api_url' => TestHelpers::getLapiUrl(), - 'stream_mode' => true, - 'use_curl' => $this->useCurl, - 'cache_system' => $cacheAdapterName, - 'redis_dsn' => getenv('REDIS_DSN'), - 'memcached_dsn' => getenv('MEMCACHED_DSN'), - 'fs_cache_path' => $this->root->url() . '/.cache' - ]; - if ($this->useTls) { - $this->addTlsConfig($bouncerConfigs, $this->useTls); - } - - $bouncer = new StandaloneBouncer($bouncerConfigs, $this->logger); - // Test cache adapter - $cacheAdapter = $bouncer->getRemediationEngine()->getCacheStorage(); - $cacheAdapter->clear(); - $this->cacheAdapterCheck($cacheAdapter, $origCacheName); - // As we are in stream mode, no live call should be done to the API. - // Warm BlockList cache up - - $bouncer->refreshBlocklistCache(); $this->assertEquals( - 'ban', - $bouncer->getRemediationForIp(TestHelpers::BAD_IP), - 'Get decisions for a bad IP for the first time (as the cache has been warmed up should be a cache hit)' + true, + $item->isHit(), + 'The captcha variables should be cached' ); - // Reconfigure the bouncer to set maximum remediation level to "captcha" - $bouncerConfigs['bouncing_level'] = Constants::BOUNCING_LEVEL_FLEX; - $bouncer = new StandaloneBouncer($bouncerConfigs, $this->logger); - $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IP); - $this->assertEquals('captcha', $cappedRemediation, 'The remediation for the banned IP should now be "captcha"'); - $bouncerConfigs['bouncing_level'] = Constants::BOUNCING_LEVEL_NORMAL; - $bouncer = new StandaloneBouncer($bouncerConfigs, $this->logger); + $cached = $item->get(); $this->assertEquals( - 'bypass', - $bouncer->getRemediationForIp(TestHelpers::CLEAN_IP), - 'Get decisions for a clean IP for the first time (as the cache has been warmed up should be a cache hit)' + true, + $cached['has_to_be_resolved'], + 'The captcha variables should be cached' ); - - // Preload the remediation to prepare the next tests. + $phraseToGuess = $cached['phrase_to_guess']; $this->assertEquals( - 'bypass', - $bouncer->getRemediationForIp(TestHelpers::NEWLY_BAD_IP), - 'Preload the bypass remediation to prepare the next tests' + 5, + strlen($phraseToGuess), + 'The captcha variables should be cached' ); - - // Add and remove decision - $this->watcherClient->setSecondState(); - - // Pull updates - $bouncer->refreshBlocklistCache(); - - $this->assertEquals( - 'ban', - $bouncer->getRemediationForIp(TestHelpers::NEWLY_BAD_IP), - 'The new decision should now be added, so the previously clean IP should now be bad' + '/', + $cached['resolution_redirect'], + 'The captcha variables should be cached' ); + $this->assertNotEmpty($cached['inline_image'], + 'The captcha variables should be cached'); $this->assertEquals( - 'bypass', - $bouncer->getRemediationForIp(TestHelpers::BAD_IP), - 'The old decisions should now be removed, so the previously bad IP should now be clean' + false, + $cached['resolution_failed'], + 'The captcha variables should be cached' ); - // Set up a new instance. - $bouncerConfigs = [ - 'auth_type' => $this->useTls ? Constants::AUTH_TLS : Constants::AUTH_KEY, - 'api_key' => TestHelpers::getBouncerKey(), - 'api_url' => TestHelpers::getLapiUrl(), - 'stream_mode' => true, - 'use_curl' => $this->useCurl, - 'cache_system' => $cacheAdapterName, - 'redis_dsn' => getenv('REDIS_DSN'), - 'memcached_dsn' => getenv('MEMCACHED_DSN'), - 'fs_cache_path' => $this->root->url() . '/.cache' - ]; - if ($this->useTls) { - $bouncerConfigs['tls_cert_path'] = $this->useTls . '/bouncer.pem'; - $bouncerConfigs['tls_key_path'] = $this->useTls . '/bouncer-key.pem'; - $bouncerConfigs['tls_ca_cert_path'] = $this->useTls . '/ca-chain.pem'; - $bouncerConfigs['tls_verify_peer'] = true; - } - - $bouncer = new StandaloneBouncer($bouncerConfigs, $this->logger); - - $this->assertEquals( - 'ban', - $bouncer->getRemediationForIp(TestHelpers::NEWLY_BAD_IP), - 'The cache warm up should be stored across each instantiation' - ); - - $this->logger->info('', ['message' => 'set "Large IPV4 range banned" + "IPV6 range banned" state']); - $this->watcherClient->deleteAllDecisions(); - $this->watcherClient->addDecision( - new \DateTime(), - '24h', - WatcherClient::HOURS24, - TestHelpers::BAD_IP . '/' . TestHelpers::LARGE_IPV4_RANGE, - 'ban' - ); - $this->watcherClient->addDecision( - new \DateTime(), - '24h', - WatcherClient::HOURS24, - TestHelpers::BAD_IPV6 . '/' . TestHelpers::IPV6_RANGE, - 'ban' - ); - // Pull updates - $bouncer->refreshBlocklistCache(); + // Step 2 :refresh + $bouncer->method('getHttpMethod')->willReturnOnConsecutiveCalls('POST', 'POST', 'POST', 'POST'); + $bouncer->method('getPostedVariable')->willReturnOnConsecutiveCalls('1', '1', '1', '1', '1', 'bad-phrase', 'bad-phrase'); - $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IP); - $this->assertEquals( - 'ban', - $cappedRemediation, - 'The remediation for the banned IP with a large range should be "ban" even in stream mode' + $_SERVER['HTTP_REFERER'] = 'UNIT-TEST'; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['refresh'] = '1'; + $_POST['crowdsec_captcha'] = '1'; + $bouncer->bounceCurrentIp(); + $cacheKeyCaptcha = $cache->getCacheKey(Constants::CACHE_TAG_CAPTCHA, TestHelpers::BAD_IP); + $item = $cache->getItem($cacheKeyCaptcha); + $cached = $item->get(); + $phraseToGuess2 = $cached['phrase_to_guess']; + $this->assertNotEquals( + $phraseToGuess2, + $phraseToGuess, + 'Phrase should have been refresh' ); - $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IPV6); $this->assertEquals( - 'bypass', - $cappedRemediation, - 'The remediation for the banned IPV6 with a too large range should now be "bypass" as we are in stream mode' + '/', + $cached['resolution_redirect'], + 'Referer is only for the first step if post' ); - // Test cache connection - $bouncer->testCacheConnection(); - - } - - /** - * @group ban - * - * @return void - * @throws BouncerException - * @throws \CrowdSec\RemediationEngine\CacheStorage\CacheStorageException - * @throws \PHPUnit\Framework\ExpectationFailedException - * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException - */ - public function testBanFlow() - { - $this->watcherClient->setSimpleDecision('ban'); - // Init bouncer - $bouncerConfigs = [ - 'auth_type' => $this->useTls ? Constants::AUTH_TLS : Constants::AUTH_KEY, - 'api_key' => TestHelpers::getBouncerKey(), - 'api_url' => TestHelpers::getLapiUrl(), - 'stream_mode' => false, - 'use_curl' => $this->useCurl, - 'cache_system' => Constants::CACHE_SYSTEM_PHPFS, - 'fs_cache_path' => $this->root->url() . '/.cache', - 'forced_test_ip' => TestHelpers::BAD_IP - ]; - if ($this->useTls) { - $this->addTlsConfig($bouncerConfigs, $this->useTls); - } + // STEP 3 : resolve captcha but failed + $_SERVER['REQUEST_METHOD'] = 'POST'; + unset($_POST['refresh']); + $_POST['phrase'] = 'bad-phrase'; + $_POST['crowdsec_captcha'] = '1'; + $bouncer->bounceCurrentIp(); - $bouncer = new StandaloneBouncerNoResponse($bouncerConfigs, $this->logger); - $bouncer->clearCache(); + $cacheKeyCaptcha = $cache->getCacheKey(Constants::CACHE_TAG_CAPTCHA, TestHelpers::BAD_IP); + $item = $cache->getItem($cacheKeyCaptcha); + $cached = $item->get(); - $cache = $bouncer->getRemediationEngine()->getCacheStorage(); - $cacheKey = $cache->getCacheKey(Constants::SCOPE_IP, TestHelpers::BAD_IP); - $item = $cache->getItem($cacheKey); $this->assertEquals( - false, - $item->isHit(), - 'The remediation should not be cached' + true, + $cached['resolution_failed'], + 'Failed should be cached' ); + // STEP 4 : resolve captcha success + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation], '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + ]); + + $bouncer->method('getHttpMethod')->willReturnOnConsecutiveCalls('POST'); + $bouncer->method('getPostedVariable')->willReturnOnConsecutiveCalls('1', null, '1', $phraseToGuess2); + + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_POST['phrase'] = $phraseToGuess2; + $_POST['crowdsec_captcha'] = '1'; $bouncer->bounceCurrentIp(); - $item = $cache->getItem($cacheKey); + $cacheKeyCaptcha = $cache->getCacheKey(Constants::CACHE_TAG_CAPTCHA, TestHelpers::BAD_IP); + $item = $cache->getItem($cacheKeyCaptcha); + $cached = $item->get(); + $this->assertEquals( - true, - $item->isHit(), - 'The remediation should be cached' + false, + $cached['has_to_be_resolved'], + 'Resolved should be cached' ); } /** - * @group captcha + * @group ban * * @return void + * * @throws BouncerException * @throws \CrowdSec\RemediationEngine\CacheStorage\CacheStorageException * @throws \PHPUnit\Framework\ExpectationFailedException * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException */ - public function testCaptchaFlow() + public function testBanFlow() { - $this->watcherClient->setSimpleDecision('captcha'); + $this->watcherClient->setSimpleDecision('ban'); // Init bouncer $bouncerConfigs = [ 'auth_type' => $this->useTls ? Constants::AUTH_TLS : Constants::AUTH_KEY, 'api_key' => TestHelpers::getBouncerKey(), 'api_url' => TestHelpers::getLapiUrl(), 'stream_mode' => false, - 'use_curl' => $this->useCurl, 'cache_system' => Constants::CACHE_SYSTEM_PHPFS, 'fs_cache_path' => $this->root->url() . '/.cache', - 'forced_test_ip' => TestHelpers::BAD_IP + 'forced_test_ip' => TestHelpers::BAD_IP, ]; if ($this->useTls) { $this->addTlsConfig($bouncerConfigs, $this->useTls); } - $bouncer = new StandaloneBouncerNoResponse($bouncerConfigs, $this->logger); - $bouncer->clearCache(); + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation], '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + ]); + $bouncer->clearCache(); $cache = $bouncer->getRemediationEngine()->getCacheStorage(); $cacheKey = $cache->getCacheKey(Constants::SCOPE_IP, TestHelpers::BAD_IP); @@ -674,18 +413,6 @@ public function testCaptchaFlow() 'The remediation should not be cached' ); - $cacheKeyCaptcha = $cache->getCacheKey(Constants::CACHE_TAG_CAPTCHA, TestHelpers::BAD_IP); - $item = $cache->getItem($cacheKeyCaptcha); - $this->assertEquals( - false, - $item->isHit(), - 'The captcha variables should not be cached' - ); - - - // Step 1 : access a page should display a captcha wall - $_SERVER['HTTP_REFERER'] = 'UNIT-TEST'; - $_SERVER['REQUEST_METHOD'] = 'GET'; $bouncer->bounceCurrentIp(); $item = $cache->getItem($cacheKey); @@ -694,103 +421,6 @@ public function testCaptchaFlow() $item->isHit(), 'The remediation should be cached' ); - - $cacheKeyCaptcha = $cache->getCacheKey(Constants::CACHE_TAG_CAPTCHA, TestHelpers::BAD_IP); - $item = $cache->getItem($cacheKeyCaptcha); - $this->assertEquals( - true, - $item->isHit(), - 'The captcha variables should be cached' - ); - - $this->assertEquals( - true, - $item->isHit(), - 'The captcha variables should be cached' - ); - - $cached = $item->get(); - $this->assertEquals( - true, - $cached['has_to_be_resolved'], - 'The captcha variables should be cached' - ); - $phraseToGuess = $cached['phrase_to_guess']; - $this->assertEquals( - 5, - strlen($phraseToGuess), - 'The captcha variables should be cached' - ); - $this->assertEquals( - '/', - $cached['resolution_redirect'], - 'The captcha variables should be cached' - ); - $this->assertNotEmpty($cached['inline_image'], - 'The captcha variables should be cached'); - - $this->assertEquals( - false, - $cached['resolution_failed'], - 'The captcha variables should be cached' - ); - - // Step 2 :refresh - $_SERVER['HTTP_REFERER'] = 'UNIT-TEST'; - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_POST['refresh'] = '1'; - $_POST['crowdsec_captcha'] = '1'; - $bouncer->bounceCurrentIp(); - $cacheKeyCaptcha = $cache->getCacheKey(Constants::CACHE_TAG_CAPTCHA, TestHelpers::BAD_IP); - $item = $cache->getItem($cacheKeyCaptcha); - $cached = $item->get(); - $phraseToGuess2 = $cached['phrase_to_guess']; - $this->assertNotEquals( - $phraseToGuess2, - $phraseToGuess, - 'Phrase should have been refresh' - ); - $this->assertEquals( - '/', - $cached['resolution_redirect'], - 'Referer is only for the first step if post' - ); - - // STEP 3 : resolve captcha but failed - $_SERVER['REQUEST_METHOD'] = 'POST'; - unset($_POST['refresh']); - $_POST['phrase'] = 'bad-phrase'; - $_POST['crowdsec_captcha'] = '1'; - $bouncer->bounceCurrentIp(); - - - $cacheKeyCaptcha = $cache->getCacheKey(Constants::CACHE_TAG_CAPTCHA, TestHelpers::BAD_IP); - $item = $cache->getItem($cacheKeyCaptcha); - $cached = $item->get(); - - $this->assertEquals( - true, - $cached['resolution_failed'], - 'Failed should be cached' - ); - - // STEP 4 : resolve captcha success - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_POST['phrase'] = $phraseToGuess2; - $_POST['crowdsec_captcha'] = '1'; - - $bouncer->bounceCurrentIp(); - - - $cacheKeyCaptcha = $cache->getCacheKey(Constants::CACHE_TAG_CAPTCHA, TestHelpers::BAD_IP); - $item = $cache->getItem($cacheKeyCaptcha); - $cached = $item->get(); - - $this->assertEquals( - false, - $cached['has_to_be_resolved'], - 'Resolved should be cached' - ); } public function testRun() @@ -805,13 +435,26 @@ public function testRun() file_exists($this->root->url() . '/' . $this->debugFile), 'Debug File should not exist' ); - // Test 1: remote ip is as expected - $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; // We have set 'trust_ip_forward_array' => ['5.6.7.8'] in $configs - $bouncer = new StandaloneBouncer($this->configs, $this->logger); - $this->assertEquals('127.0.0.1', $bouncer->getRemoteIp(), 'Get remote IP'); + // Test 2: not bouncing exclude URI - $_SERVER['REMOTE_ADDR'] = '127.0.0.2'; - $_SERVER['REQUEST_URI'] = self::EXCLUDED_URI; + $client = new BouncerClient($this->configs, null, $this->logger); + $cache = new PhpFiles($this->configs, $this->logger); + $lapiRemediation = new LapiRemediation($this->configs, $client, $cache, $this->logger); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$this->configs, $lapiRemediation, $this->logger], + '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + 'getRemoteIp', + 'getRequestUri', + ]); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls(self::EXCLUDED_URI); + $bouncer->method('getRemoteIp')->willReturnOnConsecutiveCalls('127.0.0.2'); $this->assertEquals(false, $bouncer->run(), 'Should not bounce excluded uri'); PHPUnitUtil::assertRegExp( $this, @@ -821,13 +464,46 @@ public function testRun() ); // Test 3: bouncing URI - $_SERVER['REMOTE_ADDR'] = '127.0.0.3'; - $_SERVER['REQUEST_URI'] = '/home'; + $client = new BouncerClient($this->configs, null, $this->logger); + $cache = new PhpFiles($this->configs, $this->logger); + $lapiRemediation = new LapiRemediation($this->configs, $client, $cache, $this->logger); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$this->configs, $lapiRemediation, $this->logger], + '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + 'getRemoteIp', + 'getRequestUri', + ]); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls('/home'); + $bouncer->method('getRemoteIp')->willReturnOnConsecutiveCalls('127.0.0.3'); $this->assertEquals(true, $bouncer->run(), 'Should bounce uri'); + // Test 4: not bouncing URI if disabled - $_SERVER['REMOTE_ADDR'] = '127.0.0.4'; - $bouncer = new StandaloneBouncer(array_merge($this->configs, ['bouncing_level' => - Constants::BOUNCING_LEVEL_DISABLED]), $this->logger); + $bouncerConfigs = array_merge($this->configs, ['bouncing_level' => Constants::BOUNCING_LEVEL_DISABLED]); + $client = new BouncerClient($bouncerConfigs, null, $this->logger); + $cache = new PhpFiles($bouncerConfigs, $this->logger); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache, $this->logger); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation, $this->logger], + '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + 'getRemoteIp', + 'getRequestUri', + ]); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls('/home'); + $bouncer->method('getRemoteIp')->willReturnOnConsecutiveCalls('127.0.0.4'); $this->assertEquals(false, $bouncer->run(), 'Should not bounce if disabled'); PHPUnitUtil::assertRegExp( @@ -838,16 +514,31 @@ public function testRun() ); // Test 5: throw error if config says so - $_SERVER['REMOTE_ADDR'] = '127.0.0.5'; - $bouncer = new StandaloneBouncer( - array_merge( - $this->configs, - [ - 'display_errors' => true, - 'api_url' => 'bad-url' - ] - ), $this->logger + $bouncerConfigs = array_merge( + $this->configs, + [ + 'display_errors' => true, + 'api_url' => 'bad-url', + ] ); + $client = new BouncerClient($bouncerConfigs, null, $this->logger); + $cache = new PhpFiles($bouncerConfigs, $this->logger); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache, $this->logger); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation, $this->logger], + '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + 'getRemoteIp', + 'getRequestUri', + ]); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls('/home'); + $bouncer->method('getRemoteIp')->willReturnOnConsecutiveCalls('127.0.0.5'); $error = ''; @@ -857,7 +548,7 @@ public function testRun() $error = $e->getMessage(); } - $errorExpected = $this->useCurl ? '/Could not resolve host/' : '/ailed to open stream/'; + $errorExpected = '/Could not resolve host/'; PHPUnitUtil::assertRegExp( $this, $errorExpected, @@ -870,17 +561,33 @@ public function testRun() file_get_contents($this->root->url() . '/' . $this->prodFile), 'Prod log content should be correct' ); + // Test 6: NOT throw error if config says so - $_SERVER['REMOTE_ADDR'] = '127.0.0.6'; - $bouncer = new StandaloneBouncer( - array_merge( - $this->configs, - [ - 'display_errors' => false, - 'api_url' => 'bad-url' - ] - ) - , $this->logger); + $bouncerConfigs = array_merge( + $this->configs, + [ + 'display_errors' => false, + 'api_url' => 'bad-url', + ] + ); + $client = new BouncerClient($bouncerConfigs, null, $this->logger); + $cache = new PhpFiles($bouncerConfigs, $this->logger); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache, $this->logger); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation, $this->logger], + '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + 'getRemoteIp', + 'getRequestUri', + ]); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls('/home'); + $bouncer->method('getRemoteIp')->willReturnOnConsecutiveCalls('127.0.0.6'); $error = ''; @@ -897,17 +604,33 @@ public function testRun() file_get_contents($this->root->url() . '/' . $this->prodFile), 'Prod log content should be correct' ); + // Test 7 : no-forward - $_SERVER['REMOTE_ADDR'] = '127.0.0.7'; - $bouncer = new StandaloneBouncer( - array_merge( - $this->configs, - [ - 'forced_test_forwarded_ip' => Constants::X_FORWARDED_DISABLED, - ] - ), - $this->logger + $bouncerConfigs = array_merge( + $this->configs, + [ + 'forced_test_forwarded_ip' => Constants::X_FORWARDED_DISABLED, + ] ); + $client = new BouncerClient($bouncerConfigs, null, $this->logger); + $cache = new PhpFiles($bouncerConfigs, $this->logger); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache, $this->logger); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation, $this->logger], + '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + 'getRemoteIp', + 'getRequestUri', + ]); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls('/home'); + $bouncer->method('getRemoteIp')->willReturnOnConsecutiveCalls('127.0.0.7'); + $bouncer->run(); PHPUnitUtil::assertRegExp( $this, @@ -915,16 +638,33 @@ public function testRun() file_get_contents($this->root->url() . '/' . $this->debugFile), 'Debug log content should be correct' ); + // Test 8 : forced X-Forwarded-for usage - $_SERVER['REMOTE_ADDR'] = '127.0.0.8'; - $bouncer = new StandaloneBouncer( - array_merge( - $this->configs, - [ - 'forced_test_forwarded_ip' => '1.2.3.5', - ] - ), $this->logger + $bouncerConfigs = array_merge( + $this->configs, + [ + 'forced_test_forwarded_ip' => '1.2.3.5', + ] ); + $client = new BouncerClient($bouncerConfigs, null, $this->logger); + $cache = new PhpFiles($bouncerConfigs, $this->logger); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache, $this->logger); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation, $this->logger], + '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + 'getRemoteIp', + 'getRequestUri', + ]); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls('/home'); + $bouncer->method('getRemoteIp')->willReturnOnConsecutiveCalls('127.0.0.8'); + $bouncer->run(); PHPUnitUtil::assertRegExp( $this, @@ -932,14 +672,29 @@ public function testRun() file_get_contents($this->root->url() . '/' . $this->debugFile), 'Debug log content should be correct' ); + // Test 9 non-authorized - $_SERVER['REMOTE_ADDR'] = '127.0.0.9'; - $_SERVER['HTTP_X_FORWARDED_FOR'] = '1.2.3.5'; - $bouncer = new StandaloneBouncer( - array_merge( - $this->configs - ), $this->logger - ); + $bouncerConfigs = $this->configs; + $client = new BouncerClient($bouncerConfigs, null, $this->logger); + $cache = new PhpFiles($bouncerConfigs, $this->logger); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache, $this->logger); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation, $this->logger], + '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + 'getRemoteIp', + 'getRequestUri', + ]); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls('/home'); + $bouncer->method('getRemoteIp')->willReturnOnConsecutiveCalls('127.0.0.9'); + $bouncer->method('getHttpRequestHeader')->willReturnOnConsecutiveCalls('1.2.3.5'); // HTTP_X_FORWARDED_FOR + $bouncer->run(); PHPUnitUtil::assertRegExp( $this, @@ -947,14 +702,28 @@ public function testRun() file_get_contents($this->root->url() . '/' . $this->prodFile), 'Prod log content should be correct' ); + // Test 10 authorized - $_SERVER['REMOTE_ADDR'] = '5.6.7.8'; - $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.10'; - $bouncer = new StandaloneBouncer( - array_merge( - $this->configs - ), $this->logger - ); + $bouncerConfigs = $this->configs; + $client = new BouncerClient($bouncerConfigs, null, $this->logger); + $cache = new PhpFiles($bouncerConfigs, $this->logger); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache, $this->logger); + // Mock sendResponse and redirectResponse to avoid PHP UNIT header already sent or exit error + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation, $this->logger], + '', true, + true, true, [ + 'sendResponse', + 'redirectResponse', + 'getHttpMethod', + 'getPostedVariable', + 'getHttpRequestHeader', + 'getRemoteIp', + 'getRequestUri', + ]); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls('/home'); + $bouncer->method('getRemoteIp')->willReturnOnConsecutiveCalls('5.6.7.8'); + $bouncer->method('getHttpRequestHeader')->willReturnOnConsecutiveCalls('127.0.0.10'); // HTTP_X_FORWARDED_FOR $bouncer->run(); PHPUnitUtil::assertRegExp( $this, @@ -963,4 +732,380 @@ public function testRun() 'Debug log content should be correct' ); } + + public function testPrivateAndProtectedMethods() + { + // handleCache + $configs = array_merge($this->configs, ['cache_system' => 'redis']); + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'handleCache', + [$configs, new FileLog()] + ); + + $this->assertInstanceOf(Redis::class, $result); + + $configs = array_merge($this->configs, ['cache_system' => 'memcached']); + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'handleCache', + [$configs, new FileLog()] + ); + + $this->assertInstanceOf(Memcached::class, $result); + + $configs = array_merge($this->configs, ['cache_system' => 'phpfs']); + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'handleCache', + [$configs, new FileLog()] + ); + + $this->assertInstanceOf(PhpFiles::class, $result); + + $error = ''; + + try { + $configs = array_merge($this->configs, ['cache_system' => 'phpfs']); + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + PHPUnitUtil::callMethod( + $bouncer, + 'handleCache', + [array_merge($configs, ['cache_system' => 'bad-cache-name']), new FileLog()] + ); + } catch (BouncerException $e) { + $error = $e->getMessage(); + } + + PHPUnitUtil::assertRegExp( + $this, + '/Unknown selected cache technology: bad-cache-name/', + $error, + 'Should have throw an error' + ); + + // handleClient + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'handleClient', + [$configs, new FileLog()] + ); + + $this->assertEquals(FileGetContents::class, \get_class($result->getRequestHandler())); + + $configs = array_merge($this->configs, ['use_curl' => true]); + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'handleClient', + [$configs, new FileLog()] + ); + + $this->assertEquals(Curl::class, \get_class($result->getRequestHandler())); + } + + /** + * @group integration + */ + public function testCanVerifyIpInLiveMode(): void + { + // Init context + $this->watcherClient->setInitialState(); + + // Init bouncer + $bouncerConfigs = [ + 'auth_type' => $this->useTls ? Constants::AUTH_TLS : Constants::AUTH_KEY, + 'api_key' => TestHelpers::getBouncerKey(), + 'api_url' => TestHelpers::getLapiUrl(), + 'redis_dsn' => getenv('REDIS_DSN'), + 'memcached_dsn' => getenv('MEMCACHED_DSN'), + 'fs_cache_path' => $this->root->url() . '/.cache', + 'stream_mode' => false, + ]; + if ($this->useTls) { + $this->addTlsConfig($bouncerConfigs, $this->useTls); + } + + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation]); + + // Test cache adapter + $cacheAdapter = $bouncer->getRemediationEngine()->getCacheStorage(); + $cacheAdapter->clear(); + + $this->assertEquals( + 'ban', + $bouncer->getRemediationForIp(TestHelpers::BAD_IP), + 'Get decisions for a bad IP (for the first time, it should be a cache miss)' + ); + + $this->assertEquals( + 'ban', + $bouncer->getRemediationForIp(TestHelpers::BAD_IP), + 'Call the same thing for the second time (now it should be a cache hit)' + ); + + $cleanRemediation1stCall = $bouncer->getRemediationForIp(TestHelpers::CLEAN_IP); + $this->assertEquals( + 'bypass', + $cleanRemediation1stCall, + 'Get decisions for a clean IP for the first time (it should be a cache miss)' + ); + + // Call the same thing for the second time (now it should be a cache hit) + $cleanRemediation2ndCall = $bouncer->getRemediationForIp(TestHelpers::CLEAN_IP); + $this->assertEquals('bypass', $cleanRemediation2ndCall); + + // Clear cache + $this->assertTrue($bouncer->clearCache(), 'The cache should be clearable'); + + // Call one more time (should miss as the cache has been cleared) + + $remediation3rdCall = $bouncer->getRemediationForIp(TestHelpers::BAD_IP); + $this->assertEquals('ban', $remediation3rdCall); + + // Reconfigure the bouncer to set maximum remediation level to "captcha" + $bouncerConfigs['bouncing_level'] = Constants::BOUNCING_LEVEL_FLEX; + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation]); + $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IP); + $this->assertEquals('captcha', $cappedRemediation, 'The remediation for the banned IP should now be "captcha"'); + // Reset the max remediation level to its origin state + $bouncerConfigs['bouncing_level'] = Constants::BOUNCING_LEVEL_NORMAL; + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation]); + + $this->logger->info('', ['message' => 'set "Large IPV4 range banned" state']); + $this->watcherClient->deleteAllDecisions(); + $this->watcherClient->addDecision( + new \DateTime(), + '24h', + WatcherClient::HOURS24, + TestHelpers::BAD_IP . '/' . TestHelpers::LARGE_IPV4_RANGE, + 'ban' + ); + $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IP); + $this->assertEquals( + 'ban', + $cappedRemediation, + 'The remediation for the banned IPv4 range should be ban' + ); + + $this->logger->info('', ['message' => 'set "IPV6 range banned" state']); + $this->watcherClient->deleteAllDecisions(); + $this->watcherClient->addDecision( + new \DateTime(), + '24h', + WatcherClient::HOURS24, + TestHelpers::BAD_IPV6 . '/' . TestHelpers::IPV6_RANGE, + 'ban' + ); + $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IPV6); + $this->assertEquals( + 'ban', + $cappedRemediation, + 'The remediation for a banned IPv6 range should be ban in live mode' + ); + $this->watcherClient->deleteAllDecisions(); + $this->watcherClient->addDecision( + new \DateTime(), + '24h', + WatcherClient::HOURS24, + TestHelpers::BAD_IPV6, + 'ban' + ); + $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IPV6); + $this->assertEquals( + 'ban', + $cappedRemediation, + 'The remediation for a banned IPv6 should be ban' + ); + } + + /** + * @group integration + */ + public function testCanVerifyIpInStreamMode(): void + { + // Init context + $this->watcherClient->setInitialState(); + // Init bouncer + $bouncerConfigs = [ + 'auth_type' => $this->useTls ? Constants::AUTH_TLS : Constants::AUTH_KEY, + 'api_key' => TestHelpers::getBouncerKey(), + 'api_url' => TestHelpers::getLapiUrl(), + 'stream_mode' => true, + 'redis_dsn' => getenv('REDIS_DSN'), + 'memcached_dsn' => getenv('MEMCACHED_DSN'), + 'fs_cache_path' => $this->root->url() . '/.cache', + ]; + if ($this->useTls) { + $this->addTlsConfig($bouncerConfigs, $this->useTls); + } + + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation]); + // Test cache adapter + $cacheAdapter = $bouncer->getRemediationEngine()->getCacheStorage(); + $cacheAdapter->clear(); + // As we are in stream mode, no live call should be done to the API. + // Warm BlockList cache up + + $bouncer->refreshBlocklistCache(); + + $this->assertEquals( + 'ban', + $bouncer->getRemediationForIp(TestHelpers::BAD_IP), + 'Get decisions for a bad IP for the first time (as the cache has been warmed up should be a cache hit)' + ); + + // Reconfigure the bouncer to set maximum remediation level to "captcha" + $bouncerConfigs['bouncing_level'] = Constants::BOUNCING_LEVEL_FLEX; + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation]); + $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IP); + $this->assertEquals('captcha', $cappedRemediation, 'The remediation for the banned IP should now be "captcha"'); + $bouncerConfigs['bouncing_level'] = Constants::BOUNCING_LEVEL_NORMAL; + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation]); + $this->assertEquals( + 'bypass', + $bouncer->getRemediationForIp(TestHelpers::CLEAN_IP), + 'Get decisions for a clean IP for the first time (as the cache has been warmed up should be a cache hit)' + ); + + // Preload the remediation to prepare the next tests. + $this->assertEquals( + 'bypass', + $bouncer->getRemediationForIp(TestHelpers::NEWLY_BAD_IP), + 'Preload the bypass remediation to prepare the next tests' + ); + + // Add and remove decision + $this->watcherClient->setSecondState(); + + // Pull updates + $bouncer->refreshBlocklistCache(); + + $this->assertEquals( + 'ban', + $bouncer->getRemediationForIp(TestHelpers::NEWLY_BAD_IP), + 'The new decision should now be added, so the previously clean IP should now be bad' + ); + + $this->assertEquals( + 'bypass', + $bouncer->getRemediationForIp(TestHelpers::BAD_IP), + 'The old decisions should now be removed, so the previously bad IP should now be clean' + ); + + // Set up a new instance. + $bouncerConfigs = [ + 'auth_type' => $this->useTls ? Constants::AUTH_TLS : Constants::AUTH_KEY, + 'api_key' => TestHelpers::getBouncerKey(), + 'api_url' => TestHelpers::getLapiUrl(), + 'stream_mode' => true, + 'redis_dsn' => getenv('REDIS_DSN'), + 'memcached_dsn' => getenv('MEMCACHED_DSN'), + 'fs_cache_path' => $this->root->url() . '/.cache', + ]; + if ($this->useTls) { + $bouncerConfigs['tls_cert_path'] = $this->useTls . '/bouncer.pem'; + $bouncerConfigs['tls_key_path'] = $this->useTls . '/bouncer-key.pem'; + $bouncerConfigs['tls_ca_cert_path'] = $this->useTls . '/ca-chain.pem'; + $bouncerConfigs['tls_verify_peer'] = true; + } + + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation]); + + $this->assertEquals( + 'ban', + $bouncer->getRemediationForIp(TestHelpers::NEWLY_BAD_IP) + ); + + $this->logger->info('', ['message' => 'set "Large IPV4 range banned" + "IPV6 range banned" state']); + $this->watcherClient->deleteAllDecisions(); + $this->watcherClient->addDecision( + new \DateTime(), + '24h', + WatcherClient::HOURS24, + TestHelpers::BAD_IP . '/' . TestHelpers::LARGE_IPV4_RANGE, + 'ban' + ); + $this->watcherClient->addDecision( + new \DateTime(), + '24h', + WatcherClient::HOURS24, + TestHelpers::BAD_IPV6 . '/' . TestHelpers::IPV6_RANGE, + 'ban' + ); + // Pull updates + $bouncer->refreshBlocklistCache(); + + $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IP); + $this->assertEquals( + 'ban', + $cappedRemediation, + 'The remediation for the banned IP with a large range should be "ban" even in stream mode' + ); + $cappedRemediation = $bouncer->getRemediationForIp(TestHelpers::BAD_IPV6); + $this->assertEquals( + 'bypass', + $cappedRemediation, + 'The remediation for the banned IPV6 with a too large range should now be "bypass" as we are in stream mode' + ); + + // Test cache connection + $bouncer->testCacheConnection(); + } } diff --git a/tests/Integration/GeolocationTest.php b/tests/Integration/GeolocationTest.php index 4fa0d3b..3b2a68c 100644 --- a/tests/Integration/GeolocationTest.php +++ b/tests/Integration/GeolocationTest.php @@ -4,12 +4,15 @@ namespace CrowdSecBouncer\Tests\Integration; -use CrowdSecBouncer\StandaloneBouncer; +use CrowdSec\LapiClient\Bouncer as BouncerClient; +use CrowdSec\RemediationEngine\CacheStorage\PhpFiles; +use CrowdSec\RemediationEngine\LapiRemediation; +use CrowdSecBouncer\AbstractBouncer; use CrowdSecBouncer\Constants; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; + /** - * @covers \CrowdSecBouncer\StandaloneBouncer::clearCache * @covers \CrowdSecBouncer\AbstractBouncer::getRemediationForIp * * @uses \CrowdSecBouncer\AbstractBouncer::__construct @@ -29,8 +32,7 @@ * @uses \CrowdSecBouncer\Configuration::addTemplateNodes * @uses \CrowdSecBouncer\Configuration::cleanConfigs * @uses \CrowdSecBouncer\Configuration::getConfigTreeBuilder - * @uses \CrowdSecBouncer\StandaloneBouncer::__construct - * @uses \CrowdSecBouncer\StandaloneBouncer::handleTrustedIpsConfig + * @uses \CrowdSecBouncer\AbstractBouncer::clearCache */ final class GeolocationTest extends TestCase { @@ -39,7 +41,7 @@ final class GeolocationTest extends TestCase /** @var LoggerInterface */ private $logger; - /** @var bool */ + /** @var bool */ private $useCurl; /** @var bool */ private $useTls; @@ -122,13 +124,16 @@ public function testCanVerifyIpAndCountryWithMaxmindInLiveMode(array $maxmindCon 'use_curl' => $this->useCurl, 'cache_system' => Constants::CACHE_SYSTEM_PHPFS, 'fs_cache_path' => TestHelpers::PHP_FILES_CACHE_ADAPTER_DIR, - 'stream_mode' => false + 'stream_mode' => false, ]; - $bouncer = new StandaloneBouncer($bouncerConfigs, $this->logger); + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); - $bouncer->clearCache(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation]); + $bouncer->clearCache(); $this->assertEquals( 'captcha', @@ -145,7 +150,11 @@ public function testCanVerifyIpAndCountryWithMaxmindInLiveMode(array $maxmindCon // Disable Geolocation feature $geolocationConfig['enabled'] = false; $bouncerConfigs['geolocation'] = $geolocationConfig; - $bouncer = new StandaloneBouncer($bouncerConfigs, $this->logger); + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation]); $bouncer->clearCache(); $this->assertEquals( @@ -158,7 +167,10 @@ public function testCanVerifyIpAndCountryWithMaxmindInLiveMode(array $maxmindCon $this->watcherClient->setSecondState(); $geolocationConfig['enabled'] = true; $bouncerConfigs['geolocation'] = $geolocationConfig; - $bouncer = new StandaloneBouncer($bouncerConfigs, $this->logger); + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation]); $bouncer->clearCache(); $this->assertEquals( @@ -176,6 +188,7 @@ public function testCanVerifyIpAndCountryWithMaxmindInLiveMode(array $maxmindCon /** * @group integration + * * @dataProvider maxmindConfigProvider * * @throws \Symfony\Component\Cache\Exception\CacheException|\Psr\Cache\InvalidArgumentException @@ -193,11 +206,15 @@ public function testCanVerifyIpAndCountryWithMaxmindInStreamMode(array $maxmindC 'geolocation' => $geolocationConfig, 'use_curl' => $this->useCurl, 'cache_system' => Constants::CACHE_SYSTEM_PHPFS, - 'fs_cache_path' => TestHelpers::PHP_FILES_CACHE_ADAPTER_DIR + 'fs_cache_path' => TestHelpers::PHP_FILES_CACHE_ADAPTER_DIR, ]; - $bouncer = new StandaloneBouncer($bouncerConfigs, $this->logger); - $cacheAdapter= $bouncer->getRemediationEngine()->getCacheStorage(); + $client = new BouncerClient($bouncerConfigs); + $cache = new PhpFiles($bouncerConfigs); + $lapiRemediation = new LapiRemediation($bouncerConfigs, $client, $cache); + + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$bouncerConfigs, $lapiRemediation]); + $cacheAdapter = $bouncer->getRemediationEngine()->getCacheStorage(); $cacheAdapter->clear(); // Warm BlockList cache up diff --git a/tests/Integration/StandaloneBouncerNoResponse.php b/tests/Integration/StandaloneBouncerNoResponse.php deleted file mode 100644 index 068c6e0..0000000 --- a/tests/Integration/StandaloneBouncerNoResponse.php +++ /dev/null @@ -1,31 +0,0 @@ - [Constants::CACHE_SYSTEM_PHPFS, 'PhpFilesAdapter'], 'RedisAdapter' => [Constants::CACHE_SYSTEM_REDIS, 'RedisAdapter'], @@ -51,7 +50,6 @@ public static function cacheAdapterConfigProvider(): array ]; } - public static function maxmindConfigProvider(): array { return [ diff --git a/tests/Integration/WatcherClient.php b/tests/Integration/WatcherClient.php index a17f51b..fb41d25 100644 --- a/tests/Integration/WatcherClient.php +++ b/tests/Integration/WatcherClient.php @@ -90,7 +90,7 @@ public function setSecondState(): void $this->addDecision($now, '24h', self::HOURS24, TestHelpers::IP_FRANCE, 'ban'); } - public function setSimpleDecision(string $type= 'ban'): void + public function setSimpleDecision(string $type = 'ban'): void { $this->deleteAllDecisions(); $now = new \DateTime(); @@ -131,16 +131,15 @@ public function deleteAllDecisions(): void protected function getFinalScope($scope, $value) { - $scope = (Constants::SCOPE_IP === $scope && 2 === count(explode('/', $value))) ? Constants::SCOPE_RANGE : $scope; /** * Must use capital first letter as the crowdsec agent seems to query with first capital letter - * during getStreamDecisions + * during getStreamDecisions. + * * @see https://github.com/crowdsecurity/crowdsec/blob/ae6bf3949578a5f3aa8ec415e452f15b404ba5af/pkg/database/decisions.go#L56 */ return ucfirst($scope); - } public function addDecision( diff --git a/tests/Unit/AbstractBouncerTest.php b/tests/Unit/AbstractBouncerTest.php index 38ec478..44d7976 100644 --- a/tests/Unit/AbstractBouncerTest.php +++ b/tests/Unit/AbstractBouncerTest.php @@ -5,7 +5,7 @@ namespace CrowdSecBouncer\Tests\Unit; /** - * Test for templating. + * Test for abstract bouncer. * * @author CrowdSec team * @@ -15,38 +15,79 @@ * @license MIT License */ -use CrowdSecBouncer\BouncerException; -use PHPUnit\Framework\TestCase; +use CrowdSec\Common\Logger\FileLog; +use CrowdSec\RemediationEngine\LapiRemediation; use CrowdSecBouncer\AbstractBouncer; +use CrowdSecBouncer\BouncerException; use CrowdSecBouncer\Constants; -use CrowdSec\RemediationEngine\LapiRemediation; +use CrowdSecBouncer\Tests\PHPUnitUtil; +use org\bovigo\vfs\vfsStream; +use PHPUnit\Framework\TestCase; /** * @covers \CrowdSecBouncer\AbstractBouncer::__construct - * @covers \CrowdSecBouncer\AbstractBouncer::pruneCache + * @covers \CrowdSecBouncer\AbstractBouncer::configure + * @covers \CrowdSecBouncer\AbstractBouncer::getConfig + * @covers \CrowdSecBouncer\AbstractBouncer::getConfigs + * @covers \CrowdSecBouncer\AbstractBouncer::getLogger + * @covers \CrowdSecBouncer\AbstractBouncer::getRemediationEngine + * @covers \CrowdSecBouncer\AbstractBouncer::handleCache + * @covers \CrowdSecBouncer\AbstractBouncer::handleClient + * @covers \CrowdSecBouncer\Configuration::addBouncerNodes + * @covers \CrowdSecBouncer\Configuration::addCacheNodes + * @covers \CrowdSecBouncer\Configuration::addConnectionNodes + * @covers \CrowdSecBouncer\Configuration::addDebugNodes + * @covers \CrowdSecBouncer\Configuration::addTemplateNodes + * @covers \CrowdSecBouncer\Configuration::cleanConfigs + * @covers \CrowdSecBouncer\Configuration::getConfigTreeBuilder + * @covers \CrowdSecBouncer\AbstractBouncer::shouldNotCheckResolution + * @covers \CrowdSecBouncer\AbstractBouncer::bounceCurrentIp + * @covers \CrowdSecBouncer\AbstractBouncer::capRemediationLevel + * @covers \CrowdSecBouncer\AbstractBouncer::getRemediationForIp + * @covers \CrowdSecBouncer\AbstractBouncer::getTrustForwardedIpBoundsList + * @covers \CrowdSecBouncer\AbstractBouncer::handleForwardedFor + * + * @uses \CrowdSecBouncer\AbstractBouncer::handleRemediation + * + * @covers \CrowdSecBouncer\AbstractBouncer::shouldTrustXforwardedFor + * @covers \CrowdSecBouncer\AbstractBouncer::shouldBounceCurrentIp + * @covers \CrowdSecBouncer\AbstractBouncer::checkCaptcha + * @covers \CrowdSecBouncer\AbstractBouncer::buildCaptchaCouple + * @covers \CrowdSecBouncer\Fixes\Gregwar\Captcha\CaptchaBuilder::writePhrase + * @covers \CrowdSecBouncer\AbstractBouncer::getCache + * @covers \CrowdSecBouncer\AbstractBouncer::getBanHtml + * @covers \CrowdSecBouncer\Template::__construct + * @covers \CrowdSecBouncer\Template::render + * @covers \CrowdSecBouncer\AbstractBouncer::getCaptchaHtml * @covers \CrowdSecBouncer\AbstractBouncer::clearCache + * @covers \CrowdSecBouncer\AbstractBouncer::pruneCache * @covers \CrowdSecBouncer\AbstractBouncer::refreshBlocklistCache * @covers \CrowdSecBouncer\AbstractBouncer::testCacheConnection - * - * @uses \CrowdSecBouncer\AbstractBouncer::configure - * @uses \CrowdSecBouncer\AbstractBouncer::getConfigs - * @uses \CrowdSecBouncer\AbstractBouncer::getLogger - * @covers \CrowdSecBouncer\AbstractBouncer::getRemediationEngine - * @uses \CrowdSecBouncer\Configuration::addBouncerNodes - * @uses \CrowdSecBouncer\Configuration::addCacheNodes - * @uses \CrowdSecBouncer\Configuration::addConnectionNodes - * @uses \CrowdSecBouncer\Configuration::addDebugNodes - * @uses \CrowdSecBouncer\Configuration::addTemplateNodes - * @uses \CrowdSecBouncer\Configuration::getConfigTreeBuilder - * */ final class AbstractBouncerTest extends TestCase { + private const EXCLUDED_URI = '/favicon.ico'; + /** + * @var string + */ + private $debugFile; + /** + * @var FileLog + */ + private $logger; + /** + * @var string + */ + private $prodFile; + /** + * @var vfsStreamDirectory + */ + private $root; protected $configs = [ - #============================================================================# - # Bouncer configs - #============================================================================# + // ============================================================================# + // Bouncer configs + // ============================================================================# 'use_curl' => false, 'debug_mode' => true, 'disable_prod_log' => false, @@ -55,8 +96,8 @@ final class AbstractBouncerTest extends TestCase 'forced_test_ip' => '', 'forced_test_forwarded_ip' => '', 'bouncing_level' => Constants::BOUNCING_LEVEL_NORMAL, - 'trust_ip_forward_array' => [], - 'excluded_uris' => [], + 'trust_ip_forward_array' => [['005.006.007.008', '005.006.007.008']], + 'excluded_uris' => [self::EXCLUDED_URI], 'cache_system' => Constants::CACHE_SYSTEM_PHPFS, 'captcha_cache_duration' => Constants::CACHE_EXPIRATION_FOR_CAPTCHA, 'custom_css' => '', @@ -93,9 +134,9 @@ final class AbstractBouncerTest extends TestCase 'footer' => '', ], ], - #============================================================================# - # Client configs - #============================================================================# + // ============================================================================# + // Client configs + // ============================================================================# 'auth_type' => Constants::AUTH_KEY, 'tls_cert_path' => '', 'tls_key_path' => '', @@ -104,9 +145,9 @@ final class AbstractBouncerTest extends TestCase 'api_key' => 'unit-test', 'api_url' => Constants::DEFAULT_LAPI_URL, 'api_timeout' => 1, - #============================================================================# - # Remediation engine configs - #============================================================================# + // ============================================================================# + // Remediation engine configs + // ============================================================================# 'fallback_remediation' => Constants::REMEDIATION_CAPTCHA, 'ordered_remediations' => [Constants::REMEDIATION_BAN, Constants::REMEDIATION_CAPTCHA], 'fs_cache_path' => __DIR__ . '/.cache', @@ -126,6 +167,401 @@ final class AbstractBouncerTest extends TestCase ], ]; + protected function setUp(): void + { + unset($_SERVER['REMOTE_ADDR']); + $this->root = vfsStream::setup('/tmp'); + $this->configs['log_directory_path'] = $this->root->url(); + + $currentDate = date('Y-m-d'); + $this->debugFile = 'debug-' . $currentDate . '.log'; + $this->prodFile = 'prod-' . $currentDate . '.log'; + $this->logger = new FileLog(['log_directory_path' => $this->root->url(), 'debug_mode' => true]); + } + + public function testPrivateAndProtectedMethods() + { + // shouldNotCheckResolution + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['getIpRemediation']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation], '', true, + true, true, ['getHttpMethod', 'getPostedVariable']); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'shouldNotCheckResolution', + [['has_to_be_resolved' => false]] + ); + // has_to_be_resolved = false + $this->assertEquals(true, $result); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'shouldNotCheckResolution', + [['has_to_be_resolved' => null]] + ); + // has_to_be_resolved = null + $this->assertEquals(true, $result); + + $bouncer->method('getHttpMethod')->willReturnOnConsecutiveCalls('POST', 'GET'); + $bouncer->method('getPostedVariable')->willReturnOnConsecutiveCalls('1'); + + // has_to_be_resolved = true and POST method and crowdsec_captcha = 1 + $result = PHPUnitUtil::callMethod( + $bouncer, + 'shouldNotCheckResolution', + [['has_to_be_resolved' => true]] + ); + $this->assertEquals(false, $result); + // has_to_be_resolved = true and POST method and captcha_variable = null + $result = PHPUnitUtil::callMethod( + $bouncer, + 'shouldNotCheckResolution', + [['has_to_be_resolved' => null]] + ); + $this->assertEquals(true, $result); + + // has_to_be_resolved = true and GET method + $result = PHPUnitUtil::callMethod( + $bouncer, + 'shouldNotCheckResolution', + [['has_to_be_resolved' => true]] + ); + $this->assertEquals(true, $result); + + // Classic tests + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['getIpRemediation']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation], '', true, + true, true, ['getHttpRequestHeader']); + + $bouncer->method('getHttpRequestHeader')->willReturnOnConsecutiveCalls('1.2.3.4', '1.2.3.4'); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'handleForwardedFor', + ['4.5.6.7', $configs] + ); + // 4.5.6.7 is not a trusted ip, so the result is passed ip + $this->assertEquals('4.5.6.7', $result); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'handleForwardedFor', + ['5.6.7.8', $configs] + ); + // 5.6.7.8 is a trusted ip, so the result is the forwarded ip + $this->assertEquals('1.2.3.4', $result); + + // Test disabled + $configs = array_merge($this->configs, ['forced_test_forwarded_ip' => 'no_forward']); + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['getIpRemediation']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation], '', true, + true, true, ['getHttpRequestHeader']); + + $bouncer->method('getHttpRequestHeader')->willReturnOnConsecutiveCalls('1.2.3.4', '1.2.3.4'); + $result = PHPUnitUtil::callMethod( + $bouncer, + 'handleForwardedFor', + ['4.5.6.7', $configs] + ); + // 4.5.6.7 is not a trusted ip, so the result is passed ip + $this->assertEquals('4.5.6.7', $result); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'handleForwardedFor', + ['5.6.7.8', $configs] + ); + // 5.6.7.8 is a trusted ip, so the result should be the forwarded ip but the setting is disabled + $this->assertEquals('5.6.7.8', $result); + + // Test force forwarded ip + $configs = array_merge($this->configs, ['forced_test_forwarded_ip' => '120.130.140.150']); + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['getIpRemediation']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation], '', true, + true, true, ['getHttpRequestHeader']); + + $bouncer->method('getHttpRequestHeader')->willReturnOnConsecutiveCalls('1.2.3.4', '1.2.3.4'); + $result = PHPUnitUtil::callMethod( + $bouncer, + 'handleForwardedFor', + ['4.5.6.7', $configs] + ); + // 4.5.6.7 is not a trusted ip so the result is the passed ip + $this->assertEquals('4.5.6.7', $result); + $result = PHPUnitUtil::callMethod( + $bouncer, + 'handleForwardedFor', + ['5.6.7.8', $configs] + ); + // 5.6.7.8 is a trusted ip, so the result should be the forwarded ip but the setting is a forced ip + $this->assertEquals('120.130.140.150', $result); + + // getTrustForwardedIpBoundsList + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'getTrustForwardedIpBoundsList', + [] + ); + $this->assertEquals([['005.006.007.008', '005.006.007.008']], $result); + + // capRemediationLevel + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + $this->assertInstanceOf(LapiRemediation::class, $bouncer->getRemediationEngine()); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'capRemediationLevel', + ['ban'] + ); + $this->assertEquals('ban', $result, 'Remediation should be capped as ban'); + + $this->configs['bouncing_level'] = Constants::BOUNCING_LEVEL_DISABLED; + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage', 'getConfig']) + ->getMock(); + $mockRemediation->method('getConfig')->will( + $this->returnValueMap( + [ + ['ordered_remediations', ['ban', 'captcha', 'bypass']], + ] + ) + ); + + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + $result = PHPUnitUtil::callMethod( + $bouncer, + 'capRemediationLevel', + ['ban'] + ); + $this->assertEquals('bypass', $result, 'Remediation should be capped as bypass'); + + $this->configs['bouncing_level'] = Constants::BOUNCING_LEVEL_FLEX; + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage', 'getConfig']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + $mockRemediation->method('getConfig')->will( + $this->returnValueMap( + [ + ['ordered_remediations', ['ban', 'captcha', 'bypass']], + ] + ) + ); + $result = PHPUnitUtil::callMethod( + $bouncer, + 'capRemediationLevel', + ['ban'] + ); + $this->assertEquals('captcha', $result, 'Remediation should be capped as captcha'); + + // checkCaptcha + $result = PHPUnitUtil::callMethod( + $bouncer, + 'checkCaptcha', + ['test1', 'test2', '5.6.7.8'] + ); + $this->assertEquals(false, $result, 'Captcha should be marked as not resolved'); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'checkCaptcha', + ['test1', 'test1', '5.6.7.8'] + ); + $this->assertEquals(true, $result, 'Captcha should be marked as resolved'); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'checkCaptcha', + ['test1', 'TEST1', '5.6.7.8'] + ); + $this->assertEquals(true, $result, 'Captcha should be marked as resolved even for case non matching'); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'checkCaptcha', + ['001', 'ool', '5.6.7.8'] + ); + $this->assertEquals(true, $result, 'Captcha should be marked as resolved even for some similar chars'); + + // buildCaptchaCouple + $result = PHPUnitUtil::callMethod( + $bouncer, + 'buildCaptchaCouple', + [] + ); + + $this->assertArrayHasKey('phrase', $result, 'Captcha couple should have a phrase'); + $this->assertArrayHasKey('inlineImage', $result, 'Captcha couple should have a inlineImage'); + + $this->assertIsString($result['phrase'], 'Captcha phrase should be ok'); + $this->assertEquals(5, strlen($result['phrase']), 'Captcha phrase should be of length 5'); + + $this->assertStringStartsWith('data:image/jpeg;base64', $result['inlineImage'], 'Captcha image should be ok'); + + // getCache + $result = PHPUnitUtil::callMethod( + $bouncer, + 'getCache', + [] + ); + + $this->assertInstanceOf(\CrowdSec\RemediationEngine\CacheStorage\AbstractCache::class, $result, 'Get cache should return remediation cache'); + // getBanHtml + $this->configs = array_merge($this->configs, [ + 'text' => [ + 'ban_wall' => [ + 'title' => 'BAN TEST TITLE', + ], + ], + ]); + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'getBanHtml', + [] + ); + $this->assertStringContainsString('

BAN TEST TITLE

', $result, 'Ban rendering should be as expected'); + + // getCaptchaHtml + $this->configs = array_merge($this->configs, [ + 'text' => [ + 'captcha_wall' => [ + 'title' => 'CAPTCHA TEST TITLE', + ], + ], + ]); + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + $result = PHPUnitUtil::callMethod( + $bouncer, + 'getCaptchaHtml', + [false, 'fake-inline-image', 'fake-url'] + ); + $this->assertStringContainsString('CAPTCHA TEST TITLE', $result, 'Captcha rendering should be as expected'); + $this->assertStringNotContainsString('

', $result, 'Should be no error message'); + + $result = PHPUnitUtil::callMethod( + $bouncer, + 'getCaptchaHtml', + [true, 'fake-inline-image', 'fake-url'] + ); + $this->assertStringContainsString('

', $result, 'Should be no error message'); + + // shouldTrustXforwardedFor + unset($_POST['crowdsec_captcha']); + $result = PHPUnitUtil::callMethod( + $bouncer, + 'shouldTrustXforwardedFor', + ['not-an-ip'] + ); + $this->assertEquals(false, $result, 'Should return false if ip is invalid'); + } + + public function testGetRemediationForIpExeption() + { + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['getIpRemediation']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + $this->assertInstanceOf(LapiRemediation::class, $bouncer->getRemediationEngine()); + + $mockRemediation->method('getIpRemediation')->willThrowException(new \Exception('Error in unit test', 123)); + + $errorMessage = ''; + $errorCode = 0; + try { + $bouncer->getRemediationForIp('1.2.3.3'); + } catch (BouncerException $e) { + $errorMessage = $e->getMessage(); + $errorCode = $e->getCode(); + } + + $this->assertEquals(123, $errorCode); + $this->assertEquals('Error in unit test', $errorMessage); + } + + public function testShouldBounceCurrentIp() + { + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['getIpRemediation']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + $result = $bouncer->shouldBounceCurrentIp(); + $this->assertEquals(true, $result); + + $configs = array_merge($this->configs, ['bouncing_level' => 'bouncing_disabled']); + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['getIpRemediation']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + + $result = $bouncer->shouldBounceCurrentIp(); + $this->assertEquals(false, $result); + + $configs = $this->configs; + $mockRemediation = $this->getMockBuilder(LapiRemediation::class) + ->disableOriginalConstructor() + ->onlyMethods(['getIpRemediation']) + ->getMock(); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation], '', true, + true, true, ['getRequestUri']); + + $bouncer->method('getRequestUri')->willReturnOnConsecutiveCalls(self::EXCLUDED_URI, '/good-uri'); + $result = $bouncer->shouldBounceCurrentIp(); + $this->assertEquals(false, $result); + + $result = $bouncer->shouldBounceCurrentIp(); + $this->assertEquals(true, $result); + } + public function testCacheMethodsException() { $configs = $this->configs; @@ -133,16 +569,16 @@ public function testCacheMethodsException() ->disableOriginalConstructor() ->onlyMethods(['pruneCache', 'clearCache', 'refreshDecisions', 'getCacheStorage']) ->getMock(); - $client = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); + $bouncer = $this->getMockForAbstractClass(AbstractBouncer::class, [$configs, $mockRemediation]); - $this->assertInstanceOf(LapiRemediation::class, $client->getRemediationEngine()); + $this->assertInstanceOf(LapiRemediation::class, $bouncer->getRemediationEngine()); $mockRemediation->method('pruneCache')->willThrowException(new \Exception('unit test prune cache', 123)); $errorMessage = ''; $errorCode = 0; try { - $client->pruneCache(); + $bouncer->pruneCache(); } catch (BouncerException $e) { $errorMessage = $e->getMessage(); $errorCode = $e->getCode(); @@ -156,7 +592,7 @@ public function testCacheMethodsException() $errorMessage = ''; $errorCode = 0; try { - $client->clearCache(); + $bouncer->clearCache(); } catch (BouncerException $e) { $errorMessage = $e->getMessage(); $errorCode = $e->getCode(); @@ -170,7 +606,7 @@ public function testCacheMethodsException() $errorMessage = ''; $errorCode = 0; try { - $client->refreshBlocklistCache(); + $bouncer->refreshBlocklistCache(); } catch (BouncerException $e) { $errorMessage = $e->getMessage(); $errorCode = $e->getCode(); @@ -185,7 +621,7 @@ public function testCacheMethodsException() $errorMessage = ''; $errorCode = 0; try { - $client->testCacheConnection(); + $bouncer->testCacheConnection(); } catch (BouncerException $e) { $errorMessage = $e->getMessage(); $errorCode = $e->getCode(); @@ -194,5 +630,4 @@ public function testCacheMethodsException() $this->assertEquals(101112, $errorCode); $this->assertEquals('Error while testing cache connection: unit test get cache storage', $errorMessage); } - } diff --git a/tests/Unit/StandaloneBouncerTest.php b/tests/Unit/StandaloneBouncerTest.php deleted file mode 100644 index 377a957..0000000 --- a/tests/Unit/StandaloneBouncerTest.php +++ /dev/null @@ -1,390 +0,0 @@ - false, - 'debug_mode' => true, - 'disable_prod_log' => false, - 'log_directory_path' => __DIR__ . '/.logs', - 'display_errors' => true, - 'forced_test_ip' => '', - 'forced_test_forwarded_ip' => '', - 'bouncing_level' => Constants::BOUNCING_LEVEL_NORMAL, - 'trust_ip_forward_array' => ['5.6.7.8'], - 'excluded_uris' => [self::EXCLUDED_URI], - 'cache_system' => Constants::CACHE_SYSTEM_PHPFS, - 'captcha_cache_duration' => Constants::CACHE_EXPIRATION_FOR_CAPTCHA, - 'custom_css' => '', - 'hide_mentions' => false, - 'color' => [ - 'text' => [ - 'primary' => 'black', - 'secondary' => '#AAA', - 'button' => 'white', - 'error_message' => '#b90000', - ], - 'background' => [ - 'page' => '#eee', - 'container' => 'white', - 'button' => '#626365', - 'button_hover' => '#333', - ], - ], - 'text' => [ - 'captcha_wall' => [ - 'tab_title' => 'Oops..', - 'title' => 'Hmm, sorry but...', - 'subtitle' => 'Please complete the security check.', - 'refresh_image_link' => 'refresh image', - 'captcha_placeholder' => 'Type here...', - 'send_button' => 'CONTINUE', - 'error_message' => 'Please try again.', - 'footer' => '', - ], - 'ban_wall' => [ - 'tab_title' => 'Oops..', - 'title' => '🤭 Oh!', - 'subtitle' => 'This page is protected against cyber attacks and your IP has been banned by our system.', - 'footer' => '', - ], - ], - #============================================================================# - # Client configs - #============================================================================# - 'auth_type' => Constants::AUTH_KEY, - 'tls_cert_path' => '', - 'tls_key_path' => '', - 'tls_verify_peer' => true, - 'tls_ca_cert_path' => '', - 'api_key' => 'unit-test', - 'api_url' => Constants::DEFAULT_LAPI_URL, - 'api_timeout' => 1, - #============================================================================# - # Remediation engine configs - #============================================================================# - 'fallback_remediation' => Constants::REMEDIATION_CAPTCHA, - 'ordered_remediations' => [Constants::REMEDIATION_BAN, Constants::REMEDIATION_CAPTCHA], - 'fs_cache_path' => __DIR__ . '/.cache', - 'redis_dsn' => 'redis://localhost:6379', - 'memcached_dsn' => 'memcached://localhost:11211', - 'clean_ip_cache_duration' => 1, - 'bad_ip_cache_duration' => 1, - 'stream_mode' => false, - 'geolocation' => [ - 'enabled' => false, - 'type' => Constants::GEOLOCATION_TYPE_MAXMIND, - 'cache_duration' => Constants::CACHE_EXPIRATION_FOR_GEO, - 'maxmind' => [ - 'database_type' => Constants::MAXMIND_COUNTRY, - 'database_path' => '/some/path/GeoLite2-Country.mmdb', - ], - ], - ]; - - - protected function setUp(): void - { - unset($_SERVER['REMOTE_ADDR']); - $this->root = vfsStream::setup('/tmp'); - $this->configs['log_directory_path'] = $this->root->url(); - - $currentDate = date('Y-m-d'); - $this->debugFile = 'debug-' . $currentDate . '.log'; - $this->prodFile = 'prod-' . $currentDate . '.log'; - $this->logger = new FileLog(['log_directory_path' => $this->root->url(), 'debug_mode' => true]); - } - - - public function testPrivateAndProtectedMethods() - { - - // capRemediationLevel - $bouncer = new StandaloneBouncer($this->configs); - $result = PHPUnitUtil::callMethod( - $bouncer, - 'capRemediationLevel', - ['ban'] - ); - $this->assertEquals('ban', $result, 'Remediation should be capped as ban'); - - $this->configs['bouncing_level'] = Constants::BOUNCING_LEVEL_DISABLED; - $bouncer = new StandaloneBouncer($this->configs); - $result = PHPUnitUtil::callMethod( - $bouncer, - 'capRemediationLevel', - ['ban'] - ); - $this->assertEquals('bypass', $result, 'Remediation should be capped as bypass'); - - $this->configs['bouncing_level'] = Constants::BOUNCING_LEVEL_FLEX; - $bouncer = new StandaloneBouncer($this->configs); - $result = PHPUnitUtil::callMethod( - $bouncer, - 'capRemediationLevel', - ['ban'] - ); - $this->assertEquals('captcha', $result, 'Remediation should be capped as captcha'); - - // checkCaptcha - $result = PHPUnitUtil::callMethod( - $bouncer, - 'checkCaptcha', - ['test1', 'test2', '5.6.7.8'] - ); - $this->assertEquals(false, $result, 'Captcha should be marked as not resolved'); - - $result = PHPUnitUtil::callMethod( - $bouncer, - 'checkCaptcha', - ['test1', 'test1', '5.6.7.8'] - ); - $this->assertEquals(true, $result, 'Captcha should be marked as resolved'); - - $result = PHPUnitUtil::callMethod( - $bouncer, - 'checkCaptcha', - ['test1', 'TEST1', '5.6.7.8'] - ); - $this->assertEquals(true, $result, 'Captcha should be marked as resolved even for case non matching'); - - $result = PHPUnitUtil::callMethod( - $bouncer, - 'checkCaptcha', - ['001', 'ool', '5.6.7.8'] - ); - $this->assertEquals(true, $result, 'Captcha should be marked as resolved even for some similar chars'); - - // buildCaptchaCouple - $result = PHPUnitUtil::callMethod( - $bouncer, - 'buildCaptchaCouple', - [] - ); - - $this->assertArrayHasKey('phrase', $result, 'Captcha couple should have a phrase'); - $this->assertArrayHasKey('inlineImage', $result, 'Captcha couple should have a inlineImage'); - - $this->assertIsString($result['phrase'], 'Captcha phrase should be ok'); - $this->assertEquals(5, strlen($result['phrase']), 'Captcha phrase should be of length 5'); - - - $this->assertStringStartsWith('data:image/jpeg;base64', $result['inlineImage'], 'Captcha image should be ok'); - - // getCache - $result = PHPUnitUtil::callMethod( - $bouncer, - 'getCache', - [] - ); - - $this->assertEquals('CrowdSec\RemediationEngine\CacheStorage\PhpFiles', \get_class($result), 'Get cache should return remediation cache'); - // getBanHtml - $this->configs = array_merge($this->configs, [ - 'text' => [ - 'ban_wall' => [ - 'title' => 'BAN TEST TITLE' - ] - ] - ]); - $bouncer = new StandaloneBouncer($this->configs); - - $result = PHPUnitUtil::callMethod( - $bouncer, - 'getBanHtml', - [] - ); - $this->assertStringContainsString('

BAN TEST TITLE

', $result, 'Ban rendering should be as expected'); - - // getCaptchaHtml - $this->configs = array_merge($this->configs, [ - 'text' => [ - 'captcha_wall' => [ - 'title' => 'CAPTCHA TEST TITLE' - ] - ] - ]); - $bouncer = new StandaloneBouncer($this->configs); - $result = PHPUnitUtil::callMethod( - $bouncer, - 'getCaptchaHtml', - [false, 'fake-inline-image', 'fake-url'] - - ); - $this->assertStringContainsString('CAPTCHA TEST TITLE', $result, 'Captcha rendering should be as expected'); - $this->assertStringNotContainsString('

', $result, 'Should be no error message'); - - $result = PHPUnitUtil::callMethod( - $bouncer, - 'getCaptchaHtml', - [true, 'fake-inline-image', 'fake-url'] - - ); - $this->assertStringContainsString('

', $result, 'Should be no error message'); - - // shouldNotCheckResolution - $result = PHPUnitUtil::callMethod( - $bouncer, - 'shouldNotCheckResolution', - [['has_to_be_resolved' => false]] - ); - - $this->assertEquals(true, $result, 'No check if no flagged has_to_be_resolved'); - - $_SERVER['REQUEST_METHOD'] = 'GET'; - $result = PHPUnitUtil::callMethod( - $bouncer, - 'shouldNotCheckResolution', - [['has_to_be_resolved' => true]] - ); - - $this->assertEquals(true, $result, 'No check if method is not POST'); - - $_SERVER['REQUEST_METHOD'] = 'POST'; - $_POST['crowdsec_captcha'] ='test'; - $result = PHPUnitUtil::callMethod( - $bouncer, - 'shouldNotCheckResolution', - [['has_to_be_resolved' => true]] - ); - - $this->assertEquals(false, $result, 'Check if method is POST and posted crowdsec_captcha'); - unset($_POST['crowdsec_captcha']); - $result = PHPUnitUtil::callMethod( - $bouncer, - 'shouldNotCheckResolution', - [['has_to_be_resolved' => true]] - ); - - $this->assertEquals(true, $result, 'No check if method is POST and no posted crowdsec_captcha'); - - // shouldTrustXforwardedFor - unset($_POST['crowdsec_captcha']); - $result = PHPUnitUtil::callMethod( - $bouncer, - 'shouldTrustXforwardedFor', - ['not-an-ip'] - ); - $this->assertEquals(false, $result, 'Should return false if ip is invalid'); - - // handleTrustedIpsConfig - $result = PHPUnitUtil::callMethod( - $bouncer, - 'handleTrustedIpsConfig', - [['trust_ip_forward_array' => ['1.2.3.4']]] - ); - - $this->assertEquals(['trust_ip_forward_array' => [['001.002.003.004', '001.002.003.004']]], $result, 'Should - return comparable array'); - - $error = ''; - - try { - PHPUnitUtil::callMethod( - $bouncer, - 'handleTrustedIpsConfig', - [['trust_ip_forward_array' => [['001.002.003.004', '001.002.003.004']]]] - ); - } catch (BouncerException $e) { - $error = $e->getMessage(); - } - - PHPUnitUtil::assertRegExp( - $this, - '/config must be an array of string/', - $error, - 'Should have throw an error' - ); - } - -} diff --git a/tests/Unit/TemplateTest.php b/tests/Unit/TemplateTest.php index 11e8b1c..0eeced4 100644 --- a/tests/Unit/TemplateTest.php +++ b/tests/Unit/TemplateTest.php @@ -15,26 +15,25 @@ * @license MIT License */ -use PHPUnit\Framework\TestCase; use CrowdSecBouncer\Template; +use PHPUnit\Framework\TestCase; /** - * @covers CrowdSecBouncer\Template::__construct - * @covers CrowdSecBouncer\Template::render - * + * @covers \CrowdSecBouncer\Template::__construct + * @covers \CrowdSecBouncer\Template::render */ final class TemplateTest extends TestCase { public function testRender() { - $template = new Template('ban.html.twig', __DIR__ . "/../../src/templates"); + $template = new Template('ban.html.twig', __DIR__ . '/../../src/templates'); $render = $template->render( [ 'text' => [ 'ban_wall' => [ - 'title' => 'BAN TEST TITLE' - ] - ] + 'title' => 'BAN TEST TITLE', + ], + ], ] ); @@ -46,23 +45,22 @@ public function testRender() 'text' => [ 'ban_wall' => [ 'title' => 'BAN TEST TITLE', - 'footer' => 'This is a footer test' - ] - ] + 'footer' => 'This is a footer test', + ], + ], ] ); $this->assertStringContainsString('

', $render, 'Ban rendering should contain footer'); - - $template = new Template('captcha.html.twig', __DIR__ . "/../../src/templates"); + $template = new Template('captcha.html.twig', __DIR__ . '/../../src/templates'); $render = $template->render( [ 'text' => [ 'captcha_wall' => [ - 'title' => 'CAPTCHA TEST TITLE' - ] - ] + 'title' => 'CAPTCHA TEST TITLE', + ], + ], ] ); diff --git a/tests/end-to-end/.eslintignore b/tests/end-to-end/.eslintignore deleted file mode 100644 index 3c3629e..0000000 --- a/tests/end-to-end/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/tests/end-to-end/.eslintrc b/tests/end-to-end/.eslintrc deleted file mode 100644 index bb976b9..0000000 --- a/tests/end-to-end/.eslintrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "parserOptions": { - // Required for certain syntax usages - "ecmaVersion": 2018 - }, - "extends": [ - "eslint:recommended", - "airbnb-base", - "plugin:prettier/recommended" - ], - "rules": { - "no-unused-vars": 1, - "no-underscore-dangle": 0, - "import/no-dynamic-require": 0, - "no-console": [1, { "allow": ["warn", "error", "debug"] }] - }, - "env": { - "jest": true - } -} diff --git a/tests/end-to-end/.gitignore b/tests/end-to-end/.gitignore deleted file mode 100644 index 3a8bf0b..0000000 --- a/tests/end-to-end/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules -.cookies.json -.test-results*.json -*.lock -*.log -*.jpg diff --git a/tests/end-to-end/.prettierrc b/tests/end-to-end/.prettierrc deleted file mode 100644 index 99fcc65..0000000 --- a/tests/end-to-end/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "semi": true, - "trailingComma": "all", - "printWidth": 80, - "tabWidth": 4 -} diff --git a/tests/end-to-end/CustomEnvironment.js b/tests/end-to-end/CustomEnvironment.js deleted file mode 100755 index 70125da..0000000 --- a/tests/end-to-end/CustomEnvironment.js +++ /dev/null @@ -1,30 +0,0 @@ -const PlaywrightEnvironment = - require("jest-playwright-preset/lib/PlaywrightEnvironment").default; - -class CustomEnvironment extends PlaywrightEnvironment { - async handleTestEvent(event) { - if (process.env.FAIL_FAST) { - if ( - event.name === "hook_failure" || - event.name === "test_fn_failure" - ) { - this.failedTest = true; - const buffer = await this.global.page.screenshot({ - path: "screenshot.jpg", - type: "jpeg", - quality: 20, - }); - console.debug("Screenshot:", buffer.toString("base64")); - } else if (this.failedTest && event.name === "test_start") { - // eslint-disable-next-line no-param-reassign - event.test.mode = "skip"; - } - } - - if (super.handleTestEvent) { - await super.handleTestEvent(event); - } - } -} - -module.exports = CustomEnvironment; diff --git a/tests/end-to-end/__scripts__/run-tests.sh b/tests/end-to-end/__scripts__/run-tests.sh deleted file mode 100755 index 050bc80..0000000 --- a/tests/end-to-end/__scripts__/run-tests.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash -# Run test suite -# Usage: ./run-tests.sh -# type : host, docker or ci (default: host) -# file-list : a list of test files (default: empty so it will run all the tests) -# Example: ./run-tests.sh docker "./__tests__/1-live-mode.js" - -YELLOW='\033[33m' -RESET='\033[0m' -if ! ddev --version >/dev/null 2>&1; then - printf "%bDdev is required for this script. Please see docs/ddev.md.%b\n" "${YELLOW}" "${RESET}" - exit 1 -fi - - -TYPE=${1:-host} -FILE_LIST=${2:-""} - - -case $TYPE in - "host") - echo "Running with host stack" - ;; - - "docker") - echo "Running with ddev docker stack" - ;; - - - "ci") - echo "Running in CI context" - ;; - - *) - echo "Unknown param '${TYPE}'" - echo "Usage: ./run-tests.sh " - exit 1 - ;; -esac - - -HOSTNAME=$(ddev exec printenv DDEV_HOSTNAME | sed 's/\r//g') -PHPVERSION=$(ddev exec printenv DDEV_PROJECT | sed 's/\r//g') -PHP_URL=https://$HOSTNAME -PROXY_IP=$(ddev find-ip ddev-router) -BOUNCER_KEY=$(ddev exec grep "'api_key'" /var/www/html/my-code/crowdsec-bouncer-lib/scripts/auto-prepend/settings.php | tail -1 | sed 's/api_key//g' | sed -e 's|[=>,"'\'']||g' | sed s/'\s'//g) -GEOLOC_ENABLED=$(ddev exec grep -E "'enabled'.*,$" /var/www/html/my-code/crowdsec-bouncer-lib/scripts/auto-prepend/settings.php | sed 's/enabled//g' | sed -e 's|[=>,"'\'']||g' | sed s/'\s'//g) -FORCED_TEST_FORWARDED_IP=$(ddev exec grep -E "'forced_test_forwarded_ip'.*,$" /var/www/html/my-code/crowdsec-bouncer-lib/scripts/auto-prepend/settings.php | sed 's/forced_test_forwarded_ip//g' | sed -e 's|[=>,"'\'']||g' | sed s/'\s'//g) -STREAM_MODE=$(ddev exec grep -E "'stream_mode'.*,$" /var/www/html/my-code/crowdsec-bouncer-lib/scripts/auto-prepend/settings.php | sed 's/stream_mode//g' | sed -e 's|[=>,"'\'']||g' | sed s/'\s'//g) -JEST_PARAMS="--bail=true --runInBand --verbose" -# If FAIL_FAST, will exit on first individual test fail -# @see CustomEnvironment.js -FAIL_FAST=true - -case $TYPE in - "host") - CROWDSEC_URL_FROM_HOST=$(ddev describe | grep -A 1 "crowdsec" | sed 's/Host: //g' | sed -e 's|│||g' | sed s/'\s'//g | tail -1) - cd "../" - DEBUG_STRING="PWDEBUG=1" - YARN_PATH="./" - COMMAND="yarn --cwd ${YARN_PATH} cross-env" - LAPI_URL_FROM_PLAYWRIGHT=https://${CROWDSEC_URL_FROM_HOST} - CURRENT_IP=$(ddev find-ip host) - TIMEOUT=31000 - HEADLESS=false - SLOWMO=150 - AGENT_TLS_PATH="../../../../cfssl" - ;; - - "docker") - DEBUG_STRING="" - YARN_PATH="./var/www/html/my-code/crowdsec-bouncer-lib/tests/end-to-end" - COMMAND="ddev exec -s playwright yarn --cwd ${YARN_PATH} cross-env" - LAPI_URL_FROM_PLAYWRIGHT=https://crowdsec:8080 - CURRENT_IP=$(ddev find-ip playwright) - TIMEOUT=31000 - HEADLESS=true - SLOWMO=0 - AGENT_TLS_PATH="/var/www/html/cfssl" - ;; - - "ci") - DEBUG_STRING="DEBUG=pw:api" - YARN_PATH="./var/www/html/my-code/crowdsec-bouncer-lib/tests/end-to-end" - COMMAND="ddev exec -s playwright xvfb-run --auto-servernum -- yarn --cwd ${YARN_PATH} cross-env" - LAPI_URL_FROM_PLAYWRIGHT=https://crowdsec:8080 - CURRENT_IP=$(ddev find-ip playwright) - TIMEOUT=60000 - HEADLESS=true - SLOWMO=0 - AGENT_TLS_PATH="/var/www/html/cfssl" - ;; - - *) - echo "Unknown param '${TYPE}'" - echo "Usage: ./run-tests.sh " - exit 1 - ;; -esac - - - -# Run command - -$COMMAND \ -PHP_URL="$PHP_URL" \ -$DEBUG_STRING \ -BOUNCER_KEY="$BOUNCER_KEY" \ -PROXY_IP="$PROXY_IP" \ -GEOLOC_ENABLED="$GEOLOC_ENABLED" \ -STREAM_MODE="$STREAM_MODE" \ -FORCED_TEST_FORWARDED_IP="$FORCED_TEST_FORWARDED_IP" \ -LAPI_URL_FROM_PLAYWRIGHT=$LAPI_URL_FROM_PLAYWRIGHT \ -CURRENT_IP="$CURRENT_IP" \ -TIMEOUT=$TIMEOUT \ -HEADLESS=$HEADLESS \ -FAIL_FAST=$FAIL_FAST \ -SLOWMO=$SLOWMO \ -AGENT_TLS_PATH=$AGENT_TLS_PATH \ -yarn --cwd $YARN_PATH test \ - "$JEST_PARAMS" \ - --json \ - --outputFile=./.test-results.json \ - "$FILE_LIST" diff --git a/tests/end-to-end/__scripts__/test-init.sh b/tests/end-to-end/__scripts__/test-init.sh deleted file mode 100755 index fbba567..0000000 --- a/tests/end-to-end/__scripts__/test-init.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -# Prepare Playwright container before testing -# Usage : ./test-init.sh - -YELLOW='\033[33m' -RESET='\033[0m' -if ! ddev --version >/dev/null 2>&1; then - printf "%bDdev is required for this script. Please see docs/ddev.md.%b\n" "${YELLOW}" "${RESET}" - exit 1 -fi - -ddev exec -s playwright yarn --cwd ./var/www/html/my-code/crowdsec-bouncer-lib/tests/end-to-end --force && \ -ddev exec -s playwright yarn global add cross-env diff --git a/tests/end-to-end/__tests__/1-live-mode.js b/tests/end-to-end/__tests__/1-live-mode.js deleted file mode 100644 index 369ea82..0000000 --- a/tests/end-to-end/__tests__/1-live-mode.js +++ /dev/null @@ -1,111 +0,0 @@ -/* eslint-disable no-undef */ -const { - CURRENT_IP, - FORCED_TEST_FORWARDED_IP, - STREAM_MODE, - GEOLOC_ENABLED, -} = require("../utils/constants"); - -const { - publicHomepageShouldBeBanWall, - publicHomepageShouldBeCaptchaWallWithMentions, - publicHomepageShouldBeAccessible, - publicHomepageShouldBeCaptchaWall, - banIpForSeconds, - captchaIpForSeconds, - removeAllDecisions, - wait, - runCacheAction, - fillByName, -} = require("../utils/helpers"); -const { addDecision } = require("../utils/watcherClient"); - -describe(`Live mode run`, () => { - beforeAll(async () => { - await removeAllDecisions(); - await runCacheAction("clear"); - }); - - it("Should have correct settings", async () => { - if (STREAM_MODE) { - const errorMessage = `Stream mode must be disabled for this test`; - console.error(errorMessage); - throw new Error(errorMessage); - } - if (GEOLOC_ENABLED) { - const errorMessage = "Geolocation MUST be disabled to test this."; - console.error(errorMessage); - throw new Error(errorMessage); - } - }); - - it("Should display the homepage with no remediation", async () => { - await publicHomepageShouldBeAccessible(); - }); - - it("Should display a captcha wall with mentions", async () => { - await captchaIpForSeconds( - 15 * 60, - FORCED_TEST_FORWARDED_IP || CURRENT_IP, - ); - await publicHomepageShouldBeCaptchaWallWithMentions(); - }); - - it("Should refresh image", async () => { - await runCacheAction( - "captcha-phrase", - `&ip=${FORCED_TEST_FORWARDED_IP || CURRENT_IP}`, - ); - const phrase = await page.$eval("h1", (el) => el.innerText); - await publicHomepageShouldBeCaptchaWall(); - await page.click("#refresh_link"); - await runCacheAction( - "captcha-phrase", - `&ip=${FORCED_TEST_FORWARDED_IP || CURRENT_IP}`, - ); - const newPhrase = await page.$eval("h1", (el) => el.innerText); - await expect(newPhrase).not.toEqual(phrase); - }); - - it("Should show error message", async () => { - await publicHomepageShouldBeCaptchaWall(); - expect(await page.locator(".error").count()).toBeFalsy(); - await fillByName("phrase", "bad-value"); - await page.locator('button:text("CONTINUE")').click(); - expect(await page.locator(".error").count()).toBeTruthy(); - }); - - it("Should solve the captcha", async () => { - await runCacheAction( - "captcha-phrase", - `&ip=${FORCED_TEST_FORWARDED_IP || CURRENT_IP}`, - ); - const phrase = await page.$eval("h1", (el) => el.innerText); - await publicHomepageShouldBeCaptchaWall(); - await fillByName("phrase", phrase); - await page.locator('button:text("CONTINUE")').click(); - await publicHomepageShouldBeAccessible(); - }); - - it("Should display a ban wall", async () => { - await banIpForSeconds(15 * 60, FORCED_TEST_FORWARDED_IP || CURRENT_IP); - await publicHomepageShouldBeBanWall(); - }); - - it("Should display back the homepage with no remediation", async () => { - await removeAllDecisions(); - await publicHomepageShouldBeAccessible(); - }); - - it("Should fallback to the selected remediation for unknown remediation", async () => { - await removeAllDecisions(); - await runCacheAction("clear"); - await addDecision( - FORCED_TEST_FORWARDED_IP || CURRENT_IP, - "mfa", - 15 * 60, - ); - await wait(1000); - await publicHomepageShouldBeCaptchaWall(); - }); -}); diff --git a/tests/end-to-end/__tests__/2-live-mode-with-geolocation.js b/tests/end-to-end/__tests__/2-live-mode-with-geolocation.js deleted file mode 100644 index b1c0ccc..0000000 --- a/tests/end-to-end/__tests__/2-live-mode-with-geolocation.js +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable no-undef */ -const { - GEOLOC_ENABLED, - FORCED_TEST_FORWARDED_IP, - GEOLOC_BAD_COUNTRY, - STREAM_MODE, - JAPAN_IP -} = require("../utils/constants"); - -const { - publicHomepageShouldBeBanWall, - publicHomepageShouldBeAccessible, - banIpForSeconds, - removeAllDecisions, - wait, -} = require("../utils/helpers"); -const { addDecision } = require("../utils/watcherClient"); - -describe(`Live mode run with geolocation`, () => { - beforeAll(async () => { - await removeAllDecisions(); - }); - - it("Should have correct settings", async () => { - if (STREAM_MODE) { - const errorMessage = `Stream mode must be disabled for this test`; - console.error(errorMessage); - throw new Error(errorMessage); - } - if (!GEOLOC_ENABLED) { - const errorMessage = "Geolocation MUST be enabled to test this."; - console.error(errorMessage); - throw new Error(errorMessage); - } - // Test with a Japan IP - if (FORCED_TEST_FORWARDED_IP !== JAPAN_IP) { - const errorMessage = `A forced test forwarded ip MUST be set and equals to '${JAPAN_IP}'."forced_test_forwarded_ip" setting was: ${FORCED_TEST_FORWARDED_IP}`; - console.error(errorMessage); - throw new Error(errorMessage); - } - }); - - it("Should bypass a clean IP with a clean country", async () => { - await publicHomepageShouldBeAccessible(); - }); - - it("Should ban a bad IP (ban) with a clean country", async () => { - await banIpForSeconds(15 * 60, FORCED_TEST_FORWARDED_IP); - await publicHomepageShouldBeBanWall(); - }); - - it("Should ban a clean IP with a bad country (ban)", async () => { - await removeAllDecisions(); - await addDecision(GEOLOC_BAD_COUNTRY, "ban", 15 * 60, "Country"); - await wait(1000); - await publicHomepageShouldBeBanWall(); - }); - - it("Should ban a bad IP (ban) with a bad country (captcha)", async () => { - await removeAllDecisions(); - await addDecision(GEOLOC_BAD_COUNTRY, "captcha", 15 * 60, "Country"); - await addDecision(FORCED_TEST_FORWARDED_IP, "ban", 15 * 60); - await wait(1000); - await publicHomepageShouldBeBanWall(); - }); -}); diff --git a/tests/end-to-end/__tests__/3-stream-mode.js b/tests/end-to-end/__tests__/3-stream-mode.js deleted file mode 100644 index d12775d..0000000 --- a/tests/end-to-end/__tests__/3-stream-mode.js +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable no-undef */ -const { - CURRENT_IP, - FORCED_TEST_FORWARDED_IP, - STREAM_MODE, GEOLOC_ENABLED, JAPAN_IP, -} = require("../utils/constants"); - -const { - publicHomepageShouldBeAccessible, - publicHomepageShouldBeCaptchaWall, - captchaIpForSeconds, - removeAllDecisions, - runCacheAction, -} = require("../utils/helpers"); - -describe(`Stream mode run`, () => { - beforeAll(async () => { - await removeAllDecisions(); - }); - - it("Should have correct settings", async () => { - if (!STREAM_MODE) { - const errorMessage = `Stream mode must be enabled for this test`; - console.error(errorMessage); - throw new Error(errorMessage); - } - if (GEOLOC_ENABLED) { - const errorMessage = "Geolocation MUST be disabled to test this."; - console.error(errorMessage); - throw new Error(errorMessage); - } - if (FORCED_TEST_FORWARDED_IP !== null) { - const errorMessage = `A forced test forwarded ip MUST NOT be set."forced_test_forwarded_ip" setting was: ${FORCED_TEST_FORWARDED_IP}`; - console.error(errorMessage); - throw new Error(errorMessage); - } - }); - - it("Should display the homepage with no remediation", async () => { - await runCacheAction("clear"); - await publicHomepageShouldBeAccessible(); - }); - - it("Should still bypass as cache has not been refreshed", async () => { - await captchaIpForSeconds(15 * 60, CURRENT_IP); - await publicHomepageShouldBeAccessible(); - }); - - it("Should display a captcha wall after cache refresh", async () => { - await runCacheAction("refresh"); - await publicHomepageShouldBeCaptchaWall(); - }); - - it("Should still display a captcha wall as cache has not been refreshed", async () => { - await removeAllDecisions(); - await publicHomepageShouldBeCaptchaWall(); - }); - - it("Should bypass after cache refresh", async () => { - await runCacheAction("refresh"); - await publicHomepageShouldBeAccessible(); - }); -}); diff --git a/tests/end-to-end/__tests__/4-geolocation.js b/tests/end-to-end/__tests__/4-geolocation.js deleted file mode 100644 index 5524d94..0000000 --- a/tests/end-to-end/__tests__/4-geolocation.js +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-disable no-undef */ -const { JAPAN_IP, FRANCE_IP } = require("../utils/constants"); - -const { removeAllDecisions, runGeolocationTest } = require("../utils/helpers"); - -describe(`Geolocation standalone run`, () => { - beforeAll(async () => { - await removeAllDecisions(); - }); - - it("Should get JP", async () => { - await runGeolocationTest(JAPAN_IP, false); - await expect(page).toMatchText(/Country: JP/); - }); - - it("Should get FR", async () => { - await runGeolocationTest(FRANCE_IP, false); - await expect(page).toMatchText(/Country: FR/); - }); - - it("Should call the database as we did not save result", async () => { - await runGeolocationTest(FRANCE_IP, false, true); - await expect(page).toMatchText(/Error message: The file/); - }); - - it("Should not call the GeoIp database as result is saved in cache", async () => { - await runGeolocationTest(FRANCE_IP, true); - await expect(page).toMatchText(/Country: FR/); - await runGeolocationTest(FRANCE_IP, true, true); - await expect(page).toMatchText(/Country: FR/); - }); -}); diff --git a/tests/end-to-end/__tests__/5-display-error-off.js b/tests/end-to-end/__tests__/5-display-error-off.js deleted file mode 100644 index 9d1b001..0000000 --- a/tests/end-to-end/__tests__/5-display-error-off.js +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable no-undef */ -const { publicHomepageShouldBeAccessible } = require("../utils/helpers"); - -describe(`Should not display errors`, () => { - it("Should not display error", async () => { - await publicHomepageShouldBeAccessible(); - await expect(page).not.toHaveText("body", "Fatal error"); - }); -}); diff --git a/tests/end-to-end/__tests__/6-display-error-on.js b/tests/end-to-end/__tests__/6-display-error-on.js deleted file mode 100644 index 1340264..0000000 --- a/tests/end-to-end/__tests__/6-display-error-on.js +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable no-undef */ -const { goToPublicPage } = require("../utils/helpers"); - -describe(`Should not display errors`, () => { - it("Should display error (if settings ko or something wrong while bouncing)", async () => { - await goToPublicPage(); - await expect(page).toHaveText("body", "Fatal error"); - }); -}); diff --git a/tests/end-to-end/jest-playwright.config.js b/tests/end-to-end/jest-playwright.config.js deleted file mode 100755 index 2263d0b..0000000 --- a/tests/end-to-end/jest-playwright.config.js +++ /dev/null @@ -1,18 +0,0 @@ -const headless = process.env.HEADLESS; -const slowMo = parseFloat(process.env.SLOWMO); -module.exports = { - launchOptions: { - headless, - }, - connectOptions: { slowMo }, - exitOnPageError: false, - contextOptions: { - ignoreHTTPSErrors: true, - viewport: { - width: 1920, - height: 1080, - }, - }, - browsers: ["chromium"], - devices: ["Desktop Chrome"], -}; diff --git a/tests/end-to-end/jest.config.js b/tests/end-to-end/jest.config.js deleted file mode 100755 index 9e86dc4..0000000 --- a/tests/end-to-end/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - preset: "jest-playwright-preset", - testRunner: "jest-circus/runner", - testEnvironment: "./CustomEnvironment.js", - testSequencer: "./testSequencer.js", - setupFilesAfterEnv: ["expect-playwright"], -}; diff --git a/tests/end-to-end/package.json b/tests/end-to-end/package.json deleted file mode 100644 index 0e04b1a..0000000 --- a/tests/end-to-end/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "license": "MIT", - "scripts": { - "test": "jest" - }, - "devDependencies": { - "eslint": "^7.32.0", - "eslint-config-airbnb-base": "^14.2.1", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.24.1", - "eslint-plugin-prettier": "^3.4.0", - "prettier": "^2.3.2" - }, - "dependencies": { - "@jest/test-sequencer": "^26.6.3", - "axios": "^0.21.1", - "cross-env": "^7.0.3", - "expect-playwright": "^0.8.0", - "hosted-git-info": "^2.8.9", - "jest": "^26.6.3", - "jest-circus": "^26.6.3", - "jest-environment-node": "^27.2.0", - "jest-playwright-preset": "^1.4.3", - "jest-runner": "^26.6.3", - "lodash": "^4.17.21", - "playwright-chromium": "^1.27.1", - "ws": "^7.4.6" - } -} diff --git a/tests/end-to-end/settings/base.php.dist b/tests/end-to-end/settings/base.php.dist deleted file mode 100644 index 5964ccf..0000000 --- a/tests/end-to-end/settings/base.php.dist +++ /dev/null @@ -1,84 +0,0 @@ - 'api_key', - 'api_url' => 'https://crowdsec:8080', - 'api_key' => 'REPLACE_API_KEY', - 'api_timeout' => 1, - 'use_curl' => false, - 'tls_cert_path' => '', - 'tls_key_path' => '', - 'tls_verify_peer' => true, - 'tls_ca_cert_path' => '', - // Debug/Test - 'debug_mode' => true, - 'display_errors' => true, - 'log_directory_path' => __DIR__ . '/.logs', - 'fs_cache_path' => __DIR__ . '/.cache', - 'forced_test_ip' => 'REPLACE_FORCED_IP', - 'forced_test_forwarded_ip' => 'REPLACE_FORCED_FORWARDED_IP', - // Bouncer - 'bouncing_level' => Constants::BOUNCING_LEVEL_NORMAL, - 'stream_mode' => false, - 'excluded_uris' => ['/favicon.ico'], - 'fallback_remediation' => Constants::REMEDIATION_CAPTCHA, - 'trust_ip_forward_array' => ['REPLACE_PROXY_IP'], - // Cache - 'cache_system' => Constants::CACHE_SYSTEM_PHPFS, - 'redis_dsn' => 'redis://redis:6379', - 'memcached_dsn' => 'memcached://memcached:11211', - 'clean_ip_cache_duration' => 1, - 'bad_ip_cache_duration' => 1, - 'captcha_cache_duration' => 86400, - // Geolocation - 'geolocation' => [ - 'cache_duration' => 86400, - 'enabled' => false, - 'type' => 'maxmind', - 'maxmind' => [ - 'database_type' => 'country', - 'database_path' => '/var/www/html/my-code/crowdsec-bouncer-lib/tests/GeoLite2-Country.mmdb' - ] - ], - // Settings for ban and captcha walls - 'custom_css' => '', - // true to hide CrowdSec mentions on ban and captcha walls. - 'hide_mentions' => false, - 'color' => [ - 'text' => [ - 'primary' => 'black', - 'secondary' => '#AAA', - 'button' => 'white', - 'error_message' => '#b90000', - ], - 'background' => [ - 'page' => '#eee', - 'container' => 'white', - 'button' => '#626365', - 'button_hover' => '#333', - ], - ], - 'text' => [ - // Settings for captcha wall - 'captcha_wall' => [ - 'tab_title' => 'Oops..', - 'title' => 'Hmm, sorry but...', - 'subtitle' => 'Please complete the security check.', - 'refresh_image_link' => 'refresh image', - 'captcha_placeholder' => 'Type here...', - 'send_button' => 'CONTINUE', - 'error_message' => 'Please try again.', - 'footer' => '', - ], - // Settings for ban wall - 'ban_wall' => [ - 'tab_title' => 'Oops..', - 'title' => '🤭 Oh!', - 'subtitle' => 'This page is protected against cyber attacks and your IP has been banned by our system.', - 'footer' => '', - ], - ], -]; diff --git a/tests/end-to-end/testSequencer.js b/tests/end-to-end/testSequencer.js deleted file mode 100755 index b936f37..0000000 --- a/tests/end-to-end/testSequencer.js +++ /dev/null @@ -1,14 +0,0 @@ -const Sequencer = require("@jest/test-sequencer").default; - -class CustomSequencer extends Sequencer { - sort(tests) { - // Test structure information - // https://github.com/facebook/jest/blob/6b8b1404a1d9254e7d5d90a8934087a9c9899dab/packages/jest-runner/src/types.ts#L17-L21 - const copyTests = Array.from(tests); - return copyTests.sort((testA, testB) => - testA.path > testB.path ? 1 : -1, - ); - } -} - -module.exports = CustomSequencer; diff --git a/tests/end-to-end/utils/constants.js b/tests/end-to-end/utils/constants.js deleted file mode 100644 index 8cb0ecb..0000000 --- a/tests/end-to-end/utils/constants.js +++ /dev/null @@ -1,47 +0,0 @@ -const { PHP_URL } = process.env; - -const PUBLIC_URL = - "/my-code/crowdsec-bouncer-lib/scripts/public/protected-page.php"; -const FORCED_TEST_FORWARDED_IP = - process.env.FORCED_TEST_FORWARDED_IP !== "" - ? process.env.FORCED_TEST_FORWARDED_IP - : null; -const GEOLOC_ENABLED = process.env.GEOLOC_ENABLED === "true"; -const STREAM_MODE = process.env.STREAM_MODE === "true"; -const GEOLOC_BAD_COUNTRY = "JP"; -const JAPAN_IP = "210.249.74.42"; -const FRANCE_IP = "78.119.253.85"; -const { LAPI_URL_FROM_PLAYWRIGHT } = process.env; -const { BOUNCER_KEY } = process.env; -const WATCHER_LOGIN = "watcherLogin"; -const WATCHER_PASSWORD = "watcherPassword"; -const { DEBUG } = process.env; -const { TIMEOUT } = process.env; -const { CURRENT_IP } = process.env; -const { PROXY_IP } = process.env; -const { AGENT_TLS_PATH } = process.env; -const AGENT_CERT_PATH = `${AGENT_TLS_PATH}/agent.pem`; -const AGENT_KEY_PATH = `${AGENT_TLS_PATH}/agent-key.pem`; -const CA_CERT_PATH = `${AGENT_TLS_PATH}/ca-chain.pem`; - -module.exports = { - PHP_URL, - BOUNCER_KEY, - CURRENT_IP, - DEBUG, - FORCED_TEST_FORWARDED_IP, - LAPI_URL_FROM_PLAYWRIGHT, - PROXY_IP, - PUBLIC_URL, - TIMEOUT, - WATCHER_LOGIN, - WATCHER_PASSWORD, - GEOLOC_ENABLED, - GEOLOC_BAD_COUNTRY, - STREAM_MODE, - JAPAN_IP, - FRANCE_IP, - AGENT_CERT_PATH, - AGENT_KEY_PATH, - CA_CERT_PATH, -}; diff --git a/tests/end-to-end/utils/helpers.js b/tests/end-to-end/utils/helpers.js deleted file mode 100644 index d3601cc..0000000 --- a/tests/end-to-end/utils/helpers.js +++ /dev/null @@ -1,157 +0,0 @@ -/* eslint-disable no-undef */ -const fs = require("fs"); - -const { addDecision, deleteAllDecisions } = require("./watcherClient"); -const { PHP_URL, TIMEOUT, PUBLIC_URL } = require("./constants"); - -const wait = async (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - -jest.setTimeout(TIMEOUT); - -const goToPublicPage = async (endpoint = PUBLIC_URL) => { - return page.goto(`${PHP_URL}${endpoint}`); -}; - -const runCacheAction = async (actionType = "refresh", otherParams = "") => { - await goToPublicPage( - `/my-code/crowdsec-bouncer-lib/scripts/public/cache-actions.php?action=${actionType}${otherParams}`, - ); - await page.waitForLoadState("networkidle"); - await expect(page).not.toMatchTitle(/404/); - await expect(page).toMatchTitle(`Cache action: ${actionType}`); -}; - -const runGeolocationTest = async (ip, saveResult, brokenDb = false) => { - let url = `/my-code/crowdsec-bouncer-lib/scripts/public/geolocation-test.php?ip=${ip}`; - if (saveResult) { - url += "&cache-duration=120"; - } - if (brokenDb) { - url += "&broken-db=1"; - } - await goToPublicPage(`${url}`); - await page.waitForLoadState("networkidle"); - await expect(page).not.toMatchTitle(/404/); - await expect(page).toMatchTitle(`Geolocation for IP: ${ip}`); -}; - -const computeCurrentPageRemediation = async ( - accessibleTextInTitle = "Home page", -) => { - const title = await page.title(); - if (title.includes(accessibleTextInTitle)) { - return "bypass"; - } - await expect(title).toContain("Oops"); - const description = await page.$eval(".desc", (el) => el.innerText); - const banText = "cyber"; - const captchaText = "check"; - if (description.includes(banText)) { - return "ban"; - } - if (description.includes(captchaText)) { - return "captcha"; - } - - throw Error("Current remediation can not be computed"); -}; - -const publicHomepageShouldBeBanWall = async () => { - await goToPublicPage(); - const remediation = await computeCurrentPageRemediation(); - await expect(remediation).toBe("ban"); -}; - -const publicHomepageShouldBeCaptchaWall = async () => { - await goToPublicPage(); - const remediation = await computeCurrentPageRemediation(); - await expect(remediation).toBe("captcha"); -}; - -const publicHomepageShouldBeCaptchaWallWithoutMentions = async () => { - await publicHomepageShouldBeCaptchaWall(); - await expect(page).not.toHaveText( - ".main", - "This security check has been powered by", - ); -}; - -const publicHomepageShouldBeCaptchaWallWithMentions = async () => { - await publicHomepageShouldBeCaptchaWall(); - await expect(page).toHaveText( - ".main", - "This security check has been powered by", - ); -}; - -const publicHomepageShouldBeAccessible = async () => { - await goToPublicPage(); - const remediation = await computeCurrentPageRemediation(); - await expect(remediation).toBe("bypass"); -}; - -const banIpForSeconds = async (seconds, ip) => { - await addDecision(ip, "ban", seconds); - await wait(1000); -}; - -const captchaIpForSeconds = async (seconds, ip) => { - await addDecision(ip, "captcha", seconds); - await wait(1000); -}; - -const removeAllDecisions = async () => { - await deleteAllDecisions(); - await wait(1000); -}; - -const getFileContent = async (filePath) => { - if (fs.existsSync(filePath)) { - return fs.readFileSync(filePath, "utf8"); - } - return ""; -}; - -const deleteFileContent = async (filePath) => { - if (fs.existsSync(filePath)) { - return fs.writeFileSync(filePath, ""); - } - return false; -}; - -const fillInput = async (optionId, value) => { - await page.fill(`[id=${optionId}]`, `${value}`); -}; -const fillByName = async (name, value) => { - await page.fill(`[name=${name}]`, `${value}`); -}; - -const selectElement = async (selectId, valueToSelect) => { - await page.selectOption(`[id=${selectId}]`, `${valueToSelect}`); -}; - -const selectByName = async (selectName, valueToSelect) => { - await page.selectOption(`[name=${selectName}]`, `${valueToSelect}`); -}; - -module.exports = { - addDecision, - wait, - goToPublicPage, - publicHomepageShouldBeBanWall, - publicHomepageShouldBeCaptchaWall, - publicHomepageShouldBeCaptchaWallWithoutMentions, - publicHomepageShouldBeCaptchaWallWithMentions, - publicHomepageShouldBeAccessible, - banIpForSeconds, - captchaIpForSeconds, - removeAllDecisions, - getFileContent, - deleteFileContent, - runCacheAction, - runGeolocationTest, - fillInput, - fillByName, - selectElement, - selectByName, -}; diff --git a/tests/end-to-end/utils/icon.png b/tests/end-to-end/utils/icon.png deleted file mode 100755 index 4091cd8..0000000 Binary files a/tests/end-to-end/utils/icon.png and /dev/null differ diff --git a/tests/end-to-end/utils/watcherClient.js b/tests/end-to-end/utils/watcherClient.js deleted file mode 100755 index 7ad9c0c..0000000 --- a/tests/end-to-end/utils/watcherClient.js +++ /dev/null @@ -1,209 +0,0 @@ -const axios = require("axios").default; -const https = require("https"); -const fs = require("fs"); - -const { - LAPI_URL_FROM_PLAYWRIGHT, - WATCHER_LOGIN, - WATCHER_PASSWORD, - AGENT_CERT_PATH, - AGENT_KEY_PATH, - CA_CERT_PATH, -} = require("./constants"); - -const httpsAgent = new https.Agent({ - rejectUnauthorized: true, - cert: fs.readFileSync(AGENT_CERT_PATH), - key: fs.readFileSync(AGENT_KEY_PATH), - ca: fs.readFileSync(CA_CERT_PATH), -}); - -const httpClient = axios.create({ - baseURL: LAPI_URL_FROM_PLAYWRIGHT, - timeout: 5000, - httpsAgent, -}); - -let authenticated = false; - -const long2ip = (properAddress) => { - // Converts an (IPv4) Internet network address into a string in Internet standard dotted format - // - // version: 1109.2015 - // discuss at: http://phpjs.org/functions/long2ip - // + original by: Waldo Malqui Silva - // * example 1: long2ip( 3221234342 ); - // * returns 1: '192.0.34.166' - let output = false; - if ( - !Number.isNaN(properAddress) && - (properAddress >= 0 || properAddress <= 4294967295) - ) { - output = `${Math.floor(properAddress / 256 ** 3)}.${Math.floor( - (properAddress % 256 ** 3) / 256 ** 2, - )}.${Math.floor( - ((properAddress % 256 ** 3) % 256 ** 2) / 256 ** 1, - )}.${Math.floor( - (((properAddress % 256 ** 3) % 256 ** 2) % 256 ** 1) / 256 ** 0, - )}`; - } - return output; -}; - -const ip2long = (argIpParam) => { - let i = 0; - let argIP = argIpParam; - - const pattern = new RegExp( - [ - "^([1-9]\\d*|0[0-7]*|0x[\\da-f]+)", - "(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?", - "(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?", - "(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?$", - ].join(""), - "i", - ); - argIP = argIP.match(pattern); - if (!argIP) { - throw new Error(`${argIpParam} is not a valid IP`); - } - argIP[0] = 0; - for (i = 1; i < 5; i += 1) { - argIP[0] += !!(argIP[i] || "").length; - // eslint-disable-next-line radix - argIP[i] = parseInt(argIP[i]) || 0; - } - argIP.push(256, 256, 256, 256); - argIP[4 + argIP[0]] *= 256 ** (4 - argIP[0]); - if ( - argIP[1] >= argIP[5] || - argIP[2] >= argIP[6] || - argIP[3] >= argIP[7] || - argIP[4] >= argIP[8] - ) { - throw new Error( - `Something went wrong with ${argIpParam} ip2long process`, - ); - } - return ( - argIP[1] * (argIP[0] === 1 || 16777216) + - argIP[2] * (argIP[0] <= 2 || 65536) + - argIP[3] * (argIP[0] <= 3 || 256) + - argIP[4] * 1 - ); -}; - -const cidrToRange = (cidrParam) => { - let cidr = cidrParam; - const range = [2]; - cidr = cidr.split("/"); - // eslint-disable-next-line radix - const cidr1 = parseInt(cidr[1]); - // eslint-disable-next-line no-bitwise - range[0] = long2ip(ip2long(cidr[0]) & (-1 << (32 - cidr1))); - const start = ip2long(range[0]); - range[1] = long2ip(start + 2 ** (32 - cidr1) - 1); - return range; -}; - -const auth = async () => { - if (authenticated) { - return; - } - try { - const response = await httpClient.post("/v1/watchers/login", { - machine_id: WATCHER_LOGIN, - password: WATCHER_PASSWORD, - }); - - httpClient.defaults.headers.common.Authorization = `Bearer ${response.data.token}`; - authenticated = true; - } catch (error) { - console.debug( - "WATCHER_LOGIN, WATCHER_PASSWORD", - WATCHER_LOGIN, - WATCHER_PASSWORD, - ); - console.error(error); - } -}; - -module.exports.addDecision = async ( - value, - remediation, - durationInSeconds, - scope = "Ip", -) => { - await auth(); - let finalScope = "Country"; - if (["Ip", "Range"].includes(scope)) { - // IPv6 - if (value.includes(":")) { - finalScope = "Ip"; - } else { - let startIp; - let endIp; - if (value.split("/").length === 2) { - [startIp, endIp] = cidrToRange(value); - } else { - startIp = value; - endIp = value; - } - const startLongIp = ip2long(startIp); - const endLongIp = ip2long(endIp); - const isRange = startLongIp !== endLongIp; - finalScope = isRange ? "Range" : "Ip"; - } - } - const scenario = `add ${remediation} with scope/value ${scope}/${value} for ${durationInSeconds} seconds for e2e tests`; - - const startAt = new Date(); - const stopAt = new Date(); - stopAt.setTime(stopAt.getTime() + durationInSeconds * 1000); - const body = [ - { - capacity: 0, - decisions: [ - { - duration: `${durationInSeconds}s`, - origin: "cscli", - scenario, - scope: finalScope, - type: remediation, - value, - }, - ], - events: [], - events_count: 1, - labels: null, - leakspeed: "0", - message: scenario, - scenario, - scenario_hash: "", - scenario_version: "", - simulated: false, - source: { - scope: finalScope, - value, - }, - start_at: startAt.toISOString(), - stop_at: stopAt.toISOString(), - }, - ]; - try { - await httpClient.post("/v1/alerts", body); - } catch (error) { - console.debug(error.response); - throw new Error(error); - } -}; - -module.exports.deleteAllDecisions = async () => { - try { - await auth(); - await httpClient.delete("/v1/decisions"); - } catch (error) { - console.debug(error.response); - throw new Error(error); - } -}; diff --git a/tools/coding-standards/php-cs-fixer/.php-cs-fixer.dist.php b/tools/coding-standards/php-cs-fixer/.php-cs-fixer.dist.php index f3e8f14..75a7791 100644 --- a/tools/coding-standards/php-cs-fixer/.php-cs-fixer.dist.php +++ b/tools/coding-standards/php-cs-fixer/.php-cs-fixer.dist.php @@ -22,7 +22,6 @@ ->setFinder( PhpCsFixer\Finder::create() ->in(__DIR__ . '/../../../src')->exclude(['templates']) - ->in(__DIR__ . '/../../../tests/Integration')->depth(1) - ->in(__DIR__ . '/../../../scripts')->exclude(['public']) + ->in(__DIR__ . '/../../../tests') ) ; \ No newline at end of file