diff --git a/tests/core/misc/Dockerfile b/tests/core/misc/Dockerfile new file mode 100644 index 000000000..5fd6919c0 --- /dev/null +++ b/tests/core/misc/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11.3-alpine + +WORKDIR /tmp + +COPY requirements.txt . + +RUN MAKEFLAGS="-j $(nproc)" pip install --no-cache -r requirements.txt && \ + rm -f requirements.txt + +WORKDIR /opt/tests + +COPY main.py . + +RUN apk add --no-cache curl + +ENTRYPOINT [ "python3", "main.py" ] \ No newline at end of file diff --git a/tests/core/misc/docker-compose.test.yml b/tests/core/misc/docker-compose.test.yml new file mode 100644 index 000000000..488116caa --- /dev/null +++ b/tests/core/misc/docker-compose.test.yml @@ -0,0 +1,27 @@ +version: "3.5" + +services: + tests: + build: . + environment: + PYTHONUNBUFFERED: "1" + GENERATE_SELF_SIGNED_SSL: "no" + DISABLE_DEFAULT_SERVER: "no" + REDIRECT_HTTP_TO_HTTPS: "no" + AUTO_REDIRECT_HTTP_TO_HTTPS: "yes" + ALLOWED_METHODS: "GET|POST|HEAD" + MAX_CLIENT_SIZE: "5m" + SERVE_FILES: "yes" + SSL_PROTOCOLS: "TLSv1.2 TLSv1.3" + HTTP2: "yes" + LISTEN_HTTP: "yes" + DENY_HTTP_STATUS: "403" + extra_hosts: + - "www.example.com:192.168.0.2" + networks: + bw-services: + ipv4_address: 192.168.0.3 + +networks: + bw-services: + external: true diff --git a/tests/core/misc/docker-compose.yml b/tests/core/misc/docker-compose.yml new file mode 100644 index 000000000..faa3e37b3 --- /dev/null +++ b/tests/core/misc/docker-compose.yml @@ -0,0 +1,72 @@ +version: "3.5" + +services: + bw: + image: bunkerity/bunkerweb:1.5.0-beta + pull_policy: never + labels: + - "bunkerweb.INSTANCE" + volumes: + - ./index.html:/var/www/html/index.html + environment: + API_WHITELIST_IP: "127.0.0.0/8 10.20.30.0/24 192.168.0.3" + HTTP_PORT: "80" + HTTPS_PORT: "443" + USE_BUNKERNET: "no" + USE_BLACKLIST: "no" + LOG_LEVEL: "info" + GENERATE_SELF_SIGNED_SSL: "no" + USE_MODSECURITY: "no" + + # ? MISC settings + DISABLE_DEFAULT_SERVER: "no" + REDIRECT_HTTP_TO_HTTPS: "no" + AUTO_REDIRECT_HTTP_TO_HTTPS: "yes" + ALLOWED_METHODS: "GET|POST|HEAD" + MAX_CLIENT_SIZE: "5m" + SERVE_FILES: "yes" + SSL_PROTOCOLS: "TLSv1.2 TLSv1.3" + HTTP2: "yes" + LISTEN_HTTP: "yes" + DENY_HTTP_STATUS: "403" + networks: + bw-universe: + bw-services: + ipv4_address: 192.168.0.2 + + bw-scheduler: + image: bunkerity/bunkerweb-scheduler:1.5.0-beta + pull_policy: never + depends_on: + - bw + - bw-docker + environment: + DOCKER_HOST: "tcp://bw-docker:2375" + LOG_LEVEL: "info" + networks: + - bw-universe + - bw-docker + + bw-docker: + image: tecnativa/docker-socket-proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + CONTAINERS: "1" + networks: + - bw-docker + +networks: + bw-universe: + name: bw-universe + ipam: + driver: default + config: + - subnet: 10.20.30.0/24 + bw-services: + name: bw-services + ipam: + driver: default + config: + - subnet: 192.168.0.0/24 + bw-docker: diff --git a/tests/core/misc/index.html b/tests/core/misc/index.html new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/misc/main.py b/tests/core/misc/main.py new file mode 100644 index 000000000..fd1a0a185 --- /dev/null +++ b/tests/core/misc/main.py @@ -0,0 +1,300 @@ +from os import getenv +from subprocess import run +from requests import ConnectionError, head, options, post +from socket import create_connection +from ssl import CERT_NONE, create_default_context +from time import sleep +from traceback import format_exc + +try: + ssl_generated = getenv("GENERATE_SELF_SIGNED_SSL", "no") == "yes" + disabled_default_server = getenv("DISABLE_DEFAULT_SERVER", "no") == "yes" + deny_http_status = getenv("DENY_HTTP_STATUS", "403") + listen_http = getenv("LISTEN_HTTP", "no") == "yes" + + error = False + + print( + "ℹ️ Sending a HEAD request to http://192.168.0.2 (default server) to test DISABLE_DEFAULT_SERVER", + flush=True, + ) + + try: + response = head("http://192.168.0.2") + + if response.status_code != 403 and disabled_default_server: + print( + "❌ Request didn't get rejected, even if default server is disabled, exiting ...", + flush=True, + ) + exit(1) + elif response.status_code == 403: + if not disabled_default_server: + print( + "❌ Request got rejected, even if the default server is enabled, exiting ...", + flush=True, + ) + exit(1) + + if deny_http_status != "403": + print( + f"❌ Request got rejected, but the status code shouldn't be 403 as DENY_HTTP_STATUS is set to {deny_http_status}, exiting ...", + flush=True, + ) + exit(1) + + print("✅ Request got rejected, as expected", flush=True) + else: + if not listen_http: + print( + "❌ Request didn't get rejected, even if the server is not listening on HTTP, exiting ...", + flush=True, + ) + exit(1) + + if response.status_code not in (404, 301): + response.raise_for_status() + + print("✅ Request didn't get rejected, as expected", flush=True) + except ConnectionError as e: + if listen_http: + if deny_http_status == "403" or not disabled_default_server: + raise e + + print( + "✅ Request got rejected with the expected deny_http_status", flush=True + ) + exit(0) + else: + print( + "✅ Request got rejected because the server is not listening on HTTP, as expected", + flush=True, + ) + + if ssl_generated: + sleep(1) + + ssl_protocols = getenv("SSL_PROTOCOLS", "TLSv1.2 TLSv1.3") + + print( + f"ℹ️ Creating a socket and wrapping it with SSL an SSL context to test SSL_PROTOCOLS", + flush=True, + ) + + sock = create_connection(("www.example.com", 443)) + ssl_context = create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = CERT_NONE + ssl_sock = ssl_context.wrap_socket(sock, server_hostname="www.example.com") + + if ssl_sock.version() not in ssl_protocols.split(" "): + print( + f"❌ SSL_PROTOCOLS is set to {ssl_protocols}, but the socket is using {ssl_sock.version()}, exiting ...", + flush=True, + ) + exit(1) + + print("✅ Socket is using the expected SSL protocol", flush=True) + + if not listen_http: + exit(0) + else: + print( + f"ℹ️ Skipping SSL_PROTOCOLS test as SSL is disabled", + flush=True, + ) + + sleep(1) + + redirect_http_to_https = getenv("REDIRECT_HTTP_TO_HTTPS", "no") == "yes" + auto_redirect_http_to_https = getenv("AUTO_REDIRECT_HTTP_TO_HTTPS", "no") == "yes" + + print( + f"ℹ️ Sending a HEAD request to http://www.example.com to test {'auto ' if auto_redirect_http_to_https else ''}redirect_http_to_https", + flush=True, + ) + + response = head("http://www.example.com", headers={"Host": "www.example.com"}) + + if response.status_code == 403: + print( + "✅ Request got rejected, as expected because the server is not listening on HTTP", + flush=True, + ) + else: + if response.status_code not in (404, 301): + response.raise_for_status() + + if ( + redirect_http_to_https or (auto_redirect_http_to_https and ssl_generated) + ) and response.status_code != 301: + print( + f"❌ Request didn't get redirected, even if {'auto ' if auto_redirect_http_to_https else ''}redirect_http_to_https is enabled, exiting ...", + flush=True, + ) + exit(1) + + print("✅ Request got redirected to https, as expected", flush=True) + + sleep(1) + + allowed_methods = getenv("ALLOWED_METHODS", "GET|POST|HEAD") + + print( + f"ℹ️ Sending a OPTIONS request to http{'s' if ssl_generated else ''}://www.example.com to test ALLOWED_METHODS", + flush=True, + ) + + response = options( + f"http{'s' if ssl_generated else ''}://www.example.com", + headers={"Host": "www.example.com"}, + ) + + if response.status_code == 405: + if "OPTIONS" in allowed_methods: + print( + "❌ Request got rejected, even if OPTIONS is in allowed methods, exiting ...", + flush=True, + ) + exit(1) + + print("✅ Request got rejected, as expected", flush=True) + else: + if response.status_code != 404: + response.raise_for_status() + + if "OPTIONS" not in allowed_methods: + print( + "❌ Request didn't get rejected, even if OPTIONS is not in allowed methods, exiting ...", + flush=True, + ) + exit(1) + + print("✅ Request didn't get rejected, as expected", flush=True) + + sleep(1) + + max_client_size = getenv("MAX_CLIENT_SIZE", "5m") + + print( + f"ℹ️ Sending a POST request to http{'s' if ssl_generated else ''}://www.example.com with a 5+MB body to test MAX_CLIENT_SIZE", + flush=True, + ) + + response = post( + f"http{'s' if ssl_generated else ''}://www.example.com", + headers={"Host": "www.example.com"}, + data="a" * 5242881, + verify=not ssl_generated, + ) + + if response.status_code in (413, 400): + if max_client_size != "5m": + print( + f"❌ Request got rejected, but the status code shouldn't be 400 or 413 as MAX_CLIENT_SIZE is set to {max_client_size}, exiting ...", + flush=True, + ) + exit(1) + + print("✅ Request got rejected, as expected", flush=True) + else: + if response.status_code != 404: + response.raise_for_status() + + if max_client_size == "5m": + print( + f"❌ Request didn't get rejected, even if MAX_CLIENT_SIZE is set to {max_client_size}, exiting ...", + flush=True, + ) + exit(1) + + print("✅ Request didn't get rejected, as expected", flush=True) + + sleep(1) + + serve_files = getenv("SERVE_FILES", "no") == "yes" + + print( + f"ℹ️ Sending a HEAD request to http{'s' if ssl_generated else ''}://www.example.com/index.html to test the serve_files option", + flush=True, + ) + + response = head( + f"http{'s' if ssl_generated else ''}://www.example.com/index.html", + headers={"Host": "www.example.com"}, + verify=not ssl_generated, + ) + + if response.status_code != 404 and not serve_files: + print( + "❌ Request didn't get rejected, even if serve_files is disabled, exiting ...", + flush=True, + ) + exit(1) + elif response.status_code == 404: + if serve_files: + print( + "❌ Request got rejected, even if serve_files is enabled, exiting ...", + flush=True, + ) + exit(1) + + print("✅ Request got rejected, as expected", flush=True) + else: + response.raise_for_status() + print("✅ Request didn't get rejected, as expected", flush=True) + + sleep(1) + + http2 = getenv("HTTP2", "no") == "yes" + + print( + f"ℹ️ Sending a GET request to http{'s' if ssl_generated else ''}://www.example.com with HTTP/2 to test HTTP2", + flush=True, + ) + + proc = run( + [ + "curl", + "--insecure", + "--http2", + "-I", + "-H", + '"Host: www.example.com"', + f"http{'s' if ssl_generated else ''}://www.example.com", + "-w '%{response_code} %{http_version}'", + ], + capture_output=True, + text=True, + check=True, + ) + + status_code, http_version = ( + proc.stdout.splitlines()[-1].replace("'", "").strip().split(" ") + ) + + if status_code not in ("200", "404"): + print( + f"❌ Request didn't get accepted, exiting ...", + flush=True, + ) + exit(1) + elif ssl_generated and http2 and http_version != "2": + print( + f"❌ Request didn't get accepted with HTTP/2, exiting ...", + flush=True, + ) + exit(1) + elif (not ssl_generated or not http2) and http_version != "1.1": + print( + f"❌ Request got accepted with HTTP/2, it shouldn't have, exiting ...", + flush=True, + ) + exit(1) + + print(f"✅ Request got accepted with HTTP/{http_version}", flush=True) +except SystemExit as e: + exit(e.code) +except: + print(f"❌ Something went wrong, exiting ...\n{format_exc()}", flush=True) + exit(1) diff --git a/tests/core/misc/requirements.txt b/tests/core/misc/requirements.txt new file mode 100644 index 000000000..becc27ff2 --- /dev/null +++ b/tests/core/misc/requirements.txt @@ -0,0 +1 @@ +requests==2.30.0 diff --git a/tests/core/misc/test.sh b/tests/core/misc/test.sh old mode 100644 new mode 100755 index f87f5c14c..5082c91d0 --- a/tests/core/misc/test.sh +++ b/tests/core/misc/test.sh @@ -1 +1,139 @@ -# TODO \ No newline at end of file +#!/bin/bash + +echo "🗃️ Building misc stack ..." + +# Starting stack +docker compose pull bw-docker +if [ $? -ne 0 ] ; then + echo "🗃️ Pull failed ❌" + exit 1 +fi +docker compose -f docker-compose.test.yml build +if [ $? -ne 0 ] ; then + echo "🗃️ Build failed ❌" + exit 1 +fi + +manual=0 +end=0 +cleanup_stack () { + exit_code=$? + if [[ $end -eq 1 || $exit_code = 1 ]] || [[ $end -eq 0 && $exit_code = 0 ]] && [ $manual = 0 ] ; then + find . -type f -name 'docker-compose.*' -exec sed -i 's@GENERATE_SELF_SIGNED_SSL: "yes"@GENERATE_SELF_SIGNED_SSL: "no"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@DISABLE_DEFAULT_SERVER: "yes"@DISABLE_DEFAULT_SERVER: "no"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@ALLOWED_METHODS: ".*"$@ALLOWED_METHODS: "GET|POST|HEAD"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@MAX_CLIENT_SIZE: "10m"@MAX_CLIENT_SIZE: "5m"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@SERVE_FILES: "no"@SERVE_FILES: "yes"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@SSL_PROTOCOLS: "TLSv1.2"@SSL_PROTOCOLS: "TLSv1.2 TLSv1.3"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@HTTP2: "no"@HTTP2: "yes"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@LISTEN_HTTP: "no"@LISTEN_HTTP: "yes"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@DENY_HTTP_STATUS: "444"@DENY_HTTP_STATUS: "403"@' {} \; + if [[ $end -eq 1 && $exit_code = 0 ]] ; then + return + fi + fi + + echo "🗃️ Cleaning up current stack ..." + + docker compose down -v --remove-orphans 2>/dev/null + + if [ $? -ne 0 ] ; then + echo "🗃️ Down failed ❌" + exit 1 + fi + + echo "🗃️ Cleaning up current stack done ✅" +} + +# Cleanup stack on exit +trap cleanup_stack EXIT + +for test in "default" "ssl_generated" "tweaked" "deny_status_444" "TLSv1.2" +do + if [ "$test" = "default" ] ; then + echo "🗃️ Running tests when misc settings have default values except MAX_CLIENT_SIZE which have the value \"5m\" ..." + elif [ "$test" = "ssl_generated" ] ; then + echo "🗃️ Running tests when misc settings have default values and the ssl is generated in self signed ..." + find . -type f -name 'docker-compose.*' -exec sed -i 's@GENERATE_SELF_SIGNED_SSL: "no"@GENERATE_SELF_SIGNED_SSL: "yes"@' {} \; + elif [ "$test" = "tweaked" ] ; then + echo "🗃️ Running tests when misc settings have tweaked values ..." + echo "ℹ️ Keeping the ssl generated in self signed ..." + find . -type f -name 'docker-compose.*' -exec sed -i 's@DISABLE_DEFAULT_SERVER: "no"@DISABLE_DEFAULT_SERVER: "yes"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@ALLOWED_METHODS: ".*"$@ALLOWED_METHODS: "GET|POST|HEAD|OPTIONS"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@MAX_CLIENT_SIZE: "5m"@MAX_CLIENT_SIZE: "10m"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@SERVE_FILES: "yes"@SERVE_FILES: "no"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@HTTP2: "yes"@HTTP2: "no"@' {} \; + elif [ "$test" = "deny_status_444" ] ; then + echo "🗃️ Running tests when the server's deny status is set to 444 ..." + echo "ℹ️ Keeping the ssl generated in self signed ..." + find . -type f -name 'docker-compose.*' -exec sed -i 's@DENY_HTTP_STATUS: "403"@DENY_HTTP_STATUS: "444"@' {} \; + elif [ "$test" = "TLSv1.2" ] ; then + echo "🗃️ Running tests with only TLSv1.2 enabled and when the server is not listening on http ..." + echo "ℹ️ Keeping the ssl generated in self signed ..." + find . -type f -name 'docker-compose.*' -exec sed -i 's@DISABLE_DEFAULT_SERVER: "yes"@DISABLE_DEFAULT_SERVER: "no"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@SSL_PROTOCOLS: "TLSv1.2 TLSv1.3"@SSL_PROTOCOLS: "TLSv1.2"@' {} \; + find . -type f -name 'docker-compose.*' -exec sed -i 's@LISTEN_HTTP: "yes"@LISTEN_HTTP: "no"@' {} \; + fi + + echo "🗃️ Starting stack ..." + docker compose up -d 2>/dev/null + if [ $? -ne 0 ] ; then + echo "🗃️ Up failed, retrying ... ⚠️" + manual=1 + cleanup_stack + manual=0 + docker compose up -d 2>/dev/null + if [ $? -ne 0 ] ; then + echo "🗃️ Up failed ❌" + exit 1 + fi + fi + + # Check if stack is healthy + echo "🗃️ Waiting for stack to be healthy ..." + i=0 + while [ $i -lt 120 ] ; do + containers=("misc-bw-1" "misc-bw-scheduler-1") + healthy="true" + for container in "${containers[@]}" ; do + check="$(docker inspect --format "{{json .State.Health }}" $container | grep "healthy")" + if [ "$check" = "" ] ; then + healthy="false" + break + fi + done + if [ "$healthy" = "true" ] ; then + echo "🗃️ Docker stack is healthy ✅" + break + fi + sleep 1 + i=$((i+1)) + done + if [ $i -ge 120 ] ; then + docker compose logs + echo "🗃️ Docker stack is not healthy ❌" + exit 1 + fi + + # Start tests + + docker compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from tests 2>/dev/null + + if [ $? -ne 0 ] ; then + echo "🗃️ Test \"$test\" failed ❌" + echo "🛡️ Showing BunkerWeb and BunkerWeb Scheduler logs ..." + docker compose logs bw bw-scheduler + exit 1 + else + echo "🗃️ Test \"$test\" succeeded ✅" + fi + + manual=1 + cleanup_stack + manual=0 + + echo " " +done + +end=1 +echo "🗃️ Tests are done ! ✅"