diff --git a/.gitea/workflows/workflow.yaml b/.gitea/workflows/workflow.yaml index e903441..a30f514 100644 --- a/.gitea/workflows/workflow.yaml +++ b/.gitea/workflows/workflow.yaml @@ -65,7 +65,7 @@ jobs: SCRIPT_AFTER: | set -e cd ${{ needs.prepare_context.outputs.pr_path }} - docker compose --env-file dev.env -f docker-compose.yaml up -d --build --remove-orphans + docker compose --env-file dev.env -f docker-compose.yaml up -d --build --remove-orphans --wait # ------------------------------------------------------------------ # STAGE 3: DEPLOY STAGING @@ -91,7 +91,7 @@ jobs: SCRIPT_AFTER: | set -e cd ${{ env.REMOTE_STAGING_PATH }} - docker compose --env-file staging.env -f docker-compose.yaml up -d --build --remove-orphans + docker compose --env-file staging.env -f docker-compose.yaml up -d --build --remove-orphans --wait # ------------------------------------------------------------------ # STAGE 4: DEPLOY PRODUCTION @@ -118,7 +118,16 @@ jobs: SCRIPT_AFTER: | set -e cd ${{ env.REMOTE_PROD_PATH }} - docker compose --env-file prod.env -f docker-compose.yaml -f docker-compose.prod.yaml up -d --build --remove-orphans + docker compose --env-file prod.env -f docker-compose.yaml -f docker-compose.prod.yaml up -d --build --remove-orphans --wait + + # Run E2E Tests + echo "Running E2E tests..." + export CI=true + # Create venv to avoid polluting system python + python3 -m venv .venv + . .venv/bin/activate + pip install -r tests/e2e/requirements.txt + pytest tests/e2e/ # ------------------------------------------------------------------ # CLEANUP (Using appleboy/ssh-action for pure command execution) diff --git a/dev.env b/dev.env index 6b520c4..da29fc5 100644 --- a/dev.env +++ b/dev.env @@ -6,4 +6,4 @@ CERTBOT_CA_RESOLVER=https://acme-staging-v02.api.letsencrypt.org/directory DOMAIN=dev.kovagoadi.hu ACME_BYPASS=false TRAEFIK_LEGACY_OPT= -# TRAEFIK_LEGACY_OPT="--providers.file.filename=/etc/traefik/forward-to-legacy-nginx.yaml" \ No newline at end of file +# TRAEFIK_LEGACY_OPT="--providers.file.directory=/etc/traefik" \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index d0ab6a3..85082da 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,6 +21,9 @@ services: - "--certificatesResolvers.letsencrypt.acme.caServer=${CERTBOT_CA_RESOLVER}" - "${TRAEFIK_LEGACY_OPT:-}" - "--providers.file.watch=true" + extra_hosts: + - "staging:${STAGING_IP:-192.168.1.85}" + - "webserver:${LEGACY_IP:-192.168.1.85}" ports: - "${PORT}:80" - "${HTTPS_PORT}:443" @@ -28,7 +31,7 @@ services: volumes: - "/var/run/docker.sock:/var/run/docker.sock:ro" - "letsencrypt:/letsencrypt" - - "./${ENV}/forward-to-legacy-nginx.yaml:/etc/traefik/forward-to-legacy-nginx.yaml" + - "./${ENV}:/etc/traefik" whoami: image: "traefik/whoami@sha256:200689790a0a0ea48ca45992e0450bc26ccab5307375b41c84dfc4f2475937ab" diff --git a/prod.env b/prod.env index 9a3f01a..dc8856b 100644 --- a/prod.env +++ b/prod.env @@ -5,4 +5,4 @@ NETWORK_NAME=proxy CERTBOT_CA_RESOLVER=https://acme-v02.api.letsencrypt.org/directory DOMAIN=kovagoadi.hu ACME_BYPASS=true -TRAEFIK_LEGACY_OPT="--providers.file.filename=/etc/traefik/forward-to-legacy-nginx.yaml" \ No newline at end of file +TRAEFIK_LEGACY_OPT="--providers.file.directory=/etc/traefik" \ No newline at end of file diff --git a/prod/route-to-staging-dev.yaml b/prod/route-to-staging-dev.yaml new file mode 100644 index 0000000..fd12671 --- /dev/null +++ b/prod/route-to-staging-dev.yaml @@ -0,0 +1,33 @@ +http: + routers: + # Router for HTTP (Port 80) + staging: + rule: "HostRegexp(`^.+\\.staging\\.kovagoadi\\.hu$`) || HostRegexp(`^.+\\.dev\\.kovagoadi\\.hu$`)" + entryPoints: + - "web" + service: "dev-staging" + priority: 1000 + services: + dev-staging: + loadBalancer: + servers: + - url: "http://staging:8080" + +tcp: + routers: + # Router for HTTPS (Passthrough) + dev-staging-secure: + rule: "HostSNIRegexp(`^.+\\.staging\\.kovagoadi\\.hu$`) || HostSNIRegexp(`^.+\\.dev\\.kovagoadi\\.hu$`)" + service: "dev-staging-secure" + # Passthrough must be true for SSL to reach Nginx encrypted + tls: + passthrough: true + priority: 1000 + entryPoints: + - "https" + services: + dev-staging-secure: + loadBalancer: + servers: + # Note: Ensure Traefik trusts the cert at .85 or set insecureSkipVerify + - address: "staging:445" \ No newline at end of file diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/tests/e2e/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/tests/e2e/requirements.txt b/tests/e2e/requirements.txt new file mode 100644 index 0000000..547de5c --- /dev/null +++ b/tests/e2e/requirements.txt @@ -0,0 +1,2 @@ +pytest +requests diff --git a/tests/e2e/test_traefik.py b/tests/e2e/test_traefik.py new file mode 100644 index 0000000..b8ab1db --- /dev/null +++ b/tests/e2e/test_traefik.py @@ -0,0 +1,221 @@ +import pytest +import requests +import os + +# Configuration +DOMAIN = os.getenv("DOMAIN", "dev.kovagoadi.hu") +PORT = os.getenv("PORT", "898") +HTTPS_PORT = os.getenv("HTTPS_PORT", "446") + +BASE_URL = f"http://127.0.0.1:{PORT}" +HTTPS_BASE_URL = f"https://127.0.0.1:{HTTPS_PORT}" +HOST_HEADER = f"test-whoami.{DOMAIN}" + +@pytest.fixture(scope="session", autouse=True) +def mock_webserver(): + """Start ephemeral mock webserver containers and configure Traefik.""" + import time + + # Skip ephemeral mocks in CI environment; test against real services + if os.getenv("CI"): + print("CI environment detected. Skipping ephemeral mock setup; testing against real services.") + yield + return + + # In CI (Docker-in-Docker), we need to use the HOST path for volumes, not the container path. + # The workflow mounts the project root to /app, but the Docker daemon is on the host. + # We pass PROJECT_ROOT env var to the test container to tell it where the files are on the HOST. + cwd = os.getenv("PROJECT_ROOT", os.getcwd()) + certs_dir = os.path.join(cwd, "tests/mock_nginx/certs") + image = "nginx:alpine" + + # Define mocks + mocks = [ + { + "name": "mock-legacy-ephemeral", + "alias": "webserver", + "conf": os.path.join(cwd, "tests/mock_nginx/legacy.conf"), + "ports": ["80", "443"] + }, + { + "name": "mock-staging-ephemeral", + "alias": "mock", + "conf": os.path.join(cwd, "tests/mock_nginx/staging.conf"), + "ports": ["8080", "445"] + } + ] + + # Cleanup and Start Mocks + mock_ips = {} + for mock in mocks: + subprocess.run(["docker", "rm", "-f", mock["name"]], capture_output=True) + cmd = [ + "docker", "run", "-d", "--rm", + "--name", mock["name"], + "--network", "proxy", + "--network-alias", mock["alias"], + "-v", f"{mock['conf']}:/etc/nginx/nginx.conf:ro", + "-v", f"{certs_dir}:/etc/nginx/certs:ro", + image + ] + + print(f"Starting {mock['name']}...") + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"STDOUT: {result.stdout}") + print(f"STDERR: {result.stderr}") + pytest.fail(f"Failed to start {mock['name']}: {result.stderr}") + + # Inspect container to get IP + inspect_cmd = ["docker", "inspect", "-f", "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}", mock["name"]] + inspect_res = subprocess.run(inspect_cmd, capture_output=True, text=True) + if inspect_res.returncode != 0: + pytest.fail(f"Failed to inspect {mock['name']}: {inspect_res.stderr}") + mock_ips[mock["alias"]] = inspect_res.stdout.strip() + print(f"{mock['name']} IP: {mock_ips[mock['alias']]}") + + # Restart Traefik with IP env vars for extra_hosts + print(f"Restarting Traefik with STAGING_IP={mock_ips['mock']} and LEGACY_IP={mock_ips['webserver']}...") + env = os.environ.copy() + env["STAGING_IP"] = mock_ips["mock"] + env["LEGACY_IP"] = mock_ips["webserver"] + + # In CI (Docker-in-Docker), we need to tell docker-compose that the project root + # is the HOST path (PROJECT_ROOT), not the container path (/app). + # This ensures volume mounts use the correct host paths. + if "PROJECT_ROOT" in os.environ: + env["COMPOSE_PROJECT_DIR"] = os.environ["PROJECT_ROOT"] + + # We need to recreate the container to pick up extra_hosts changes + try: + subprocess.run(["docker-compose", "--env-file", "dev.env", "up", "-d", "--force-recreate", "--no-deps", "traefik"], env=env, check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + print(f"Docker Compose STDOUT: {e.stdout}") + print(f"Docker Compose STDERR: {e.stderr}") + raise e + + # Wait for everything to settle + time.sleep(5) + + yield + + print("Stopping mocks and restoring Traefik...") + for mock in mocks: + subprocess.run(["docker", "stop", mock["name"]], capture_output=True) + + # Restore Traefik to default (optional) + subprocess.run(["docker-compose", "--env-file", "dev.env", "up", "-d", "--force-recreate", "--no-deps", "traefik"], check=True) + +def test_whoami_http_reachable(): + """Verify that the whoami service is reachable via HTTP.""" + try: + response = requests.get(BASE_URL, headers={"Host": HOST_HEADER}, timeout=5) + assert response.status_code == 200 + assert "Hostname:" in response.text + except requests.exceptions.RequestException as e: + pytest.fail(f"Failed to connect to {BASE_URL}: {e}") + +import subprocess + +def test_whoami_https_reachable(): + """Verify that the whoami service is reachable via HTTPS using curl (to handle SNI/DNS).""" + # We use curl because requests doesn't support --resolve easily to force SNI with custom IP + cmd = [ + "curl", "-s", "-k", "--fail", + "--resolve", f"{HOST_HEADER}:{HTTPS_PORT}:127.0.0.1", + f"https://{HOST_HEADER}:{HTTPS_PORT}" + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10, check=True) + assert result.returncode == 0 + except subprocess.CalledProcessError as e: + pytest.fail(f"Curl failed with exit code {e.returncode}: {e.stderr}") + except subprocess.TimeoutExpired: + pytest.fail("Curl timed out") + +def test_https_redirect(): + """Verify that HTTP requests are NOT redirected to HTTPS (based on config, or check if they SHOULD be).""" + # Based on docker-compose, there is no global redirect middleware configured on the entrypoint. + # So HTTP should remain HTTP. + try: + response = requests.get(BASE_URL, headers={"Host": HOST_HEADER}, allow_redirects=False, timeout=5) + assert response.status_code == 200 + except requests.exceptions.RequestException as e: + pytest.fail(f"Failed to connect to {BASE_URL}: {e}") + +def test_unknown_host_404(): + """Verify that requests to an unknown host (hitting legacy-nginx catch-all) are routed to the mock.""" + try: + # We use a domain that doesn't match the specific *.dev.kovagoadi.hu rules + # so it falls through to the catch-all 'nginx-legacy-router' + response = requests.get(BASE_URL + "/login", headers={"Host": "tar.kovagoadi.hu"}, timeout=5) + assert response.status_code == 200 + except requests.exceptions.RequestException as e: + pytest.fail(f"Failed to connect to {BASE_URL}: {e}") + +def test_unknown_host_https_passthrough(): + """Verify that HTTPS requests to an unknown host are passed through to the mock Nginx.""" + # We use curl to handle SNI and self-signed certs + # unknown.com should hit the catch-all SNI router + cmd = [ + "curl", "-s", "-k", "-i", "--fail", + "--resolve", f"tar.kovagoadi.hu:{HTTPS_PORT}:127.0.0.1", + f"https://tar.kovagoadi.hu:{HTTPS_PORT}/login" + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10, check=True) + assert result.returncode == 0 + except subprocess.CalledProcessError as e: + pytest.fail(f"Curl failed with exit code {e.returncode}: {e.stderr}") + except subprocess.TimeoutExpired: + pytest.fail("Curl timed out") + +def test_staging_http_routing(): + """Verify HTTP requests to *.staging.kovagoadi.hu are routed to the mock.""" + host = "test-whoami.staging.kovagoadi.hu" + try: + response = requests.get(BASE_URL, headers={"Host": host}, timeout=5) + assert response.status_code == 200 + except requests.exceptions.RequestException as e: + pytest.fail(f"Failed to connect to {BASE_URL} with host {host}: {e}") + +def test_dev_http_routing(): + """Verify HTTP requests to *.dev.kovagoadi.hu are routed to the mock.""" + host = "test-whoami.dev.kovagoadi.hu" + try: + response = requests.get(BASE_URL, headers={"Host": host}, timeout=5) + assert response.status_code == 200 + except requests.exceptions.RequestException as e: + pytest.fail(f"Failed to connect to {BASE_URL} with host {host}: {e}") + +def test_staging_https_routing(): + """Verify HTTPS requests to *.staging.kovagoadi.hu are routed to the mock.""" + host = "test-whoami.staging.kovagoadi.hu" + cmd = [ + "curl", "-s", "-k", "-i", "--fail", + "--resolve", f"{host}:{HTTPS_PORT}:127.0.0.1", + f"https://{host}:{HTTPS_PORT}" + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10, check=True) + assert result.returncode == 0 + except subprocess.CalledProcessError as e: + pytest.fail(f"Curl failed with exit code {e.returncode}: {e.stderr}") + except subprocess.TimeoutExpired: + pytest.fail("Curl timed out") + +def test_dev_https_routing(): + """Verify HTTPS requests to *.dev.kovagoadi.hu are routed to the mock.""" + host = "test-whoami.dev.kovagoadi.hu" + cmd = [ + "curl", "-s", "-k", "-i", "--fail", + "--resolve", f"{host}:{HTTPS_PORT}:127.0.0.1", + f"https://{host}:{HTTPS_PORT}" + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10, check=True) + assert result.returncode == 0 + except subprocess.CalledProcessError as e: + pytest.fail(f"Curl failed with exit code {e.returncode}: {e.stderr}") + except subprocess.TimeoutExpired: + pytest.fail("Curl timed out") diff --git a/tests/mock_nginx/certs/server.crt b/tests/mock_nginx/certs/server.crt new file mode 100644 index 0000000..b30b81b --- /dev/null +++ b/tests/mock_nginx/certs/server.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCzCCAfOgAwIBAgIUU5vuywVWaoDfFyee7NCy0BqXU3kwDQYJKoZIhvcNAQEL +BQAwFTETMBEGA1UEAwwKbW9jay1uZ2lueDAeFw0yNTEyMjUxNjAwMTNaFw0yNjEy +MjUxNjAwMTNaMBUxEzARBgNVBAMMCm1vY2stbmdpbngwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCaoLrs78+HbDFB/HsXTy5nqtP7V33Js0GDWxk6QkOq +qbCtd5e2fzjkFFWkFIuouLEL1+4xW5txToaCtIYhQUgwST/Wu4wLlzrueRmprTh2 +oTLbpSVXn6kO7dvDinsfelcjnLNgjRkkKPRYVvnCsrr2O9gFhClWf+WKMuNP99/O +4Y7Na4l6fyQAMjDSgdFjVvB+D9YMbkfNmo5hs009lF0Y6u35hIaY1NyfI+csvwWZ +j75I/1TGKQGwrmtG2U31xM8Y8mj7mye0IeXoIg8rl/efYVKRcEBahu0140t6A69g +N7kCSsGhl78VJEUVz5OuOSdV9UDwzawiHh/M+1KE3/3VAgMBAAGjUzBRMB0GA1Ud +DgQWBBQOgMuir6oD712l2GKpUoPo0RUcyDAfBgNVHSMEGDAWgBQOgMuir6oD712l +2GKpUoPo0RUcyDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAv +52u9mAOTKStdNZA4R68icJRXmOuxkTh9/rMdOYFF+GM2bMiX1DsvY9/VAFuy7EdZ +yfvu72JyaBDMljryz6doupXm9Do9um2sTU0YKkarV2EECYqt5uC56KWs9XZ9mnYW +wnTrLTbGZhRbGz4pq+Pq7Z1VUexV/Tnoxs5RlOaDxrlaS9htLfIR3dv2bhQiMGN9 +1LESNZ16c3s4026pxeojCKFVf2GS+IBdbzljdUzBMVzwGl10UZQHfMDJptuW6JKo +rO5XVp5cLpLzSgUSGtGIoT2dR2Nk5kxGw4VbRCKKD7CDdS+JFypSwdg3v2oNfAmV +op1lOPZtztQRw/6zo4Ex +-----END CERTIFICATE----- diff --git a/tests/mock_nginx/certs/server.key b/tests/mock_nginx/certs/server.key new file mode 100644 index 0000000..b423548 --- /dev/null +++ b/tests/mock_nginx/certs/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCaoLrs78+HbDFB +/HsXTy5nqtP7V33Js0GDWxk6QkOqqbCtd5e2fzjkFFWkFIuouLEL1+4xW5txToaC +tIYhQUgwST/Wu4wLlzrueRmprTh2oTLbpSVXn6kO7dvDinsfelcjnLNgjRkkKPRY +VvnCsrr2O9gFhClWf+WKMuNP99/O4Y7Na4l6fyQAMjDSgdFjVvB+D9YMbkfNmo5h +s009lF0Y6u35hIaY1NyfI+csvwWZj75I/1TGKQGwrmtG2U31xM8Y8mj7mye0IeXo +Ig8rl/efYVKRcEBahu0140t6A69gN7kCSsGhl78VJEUVz5OuOSdV9UDwzawiHh/M ++1KE3/3VAgMBAAECggEANx57c5Fqj1IMXwLC2ATEPHUDGpHOB5PMEyhqnj9XwqK5 +laRPYuEH5Rmwi4w9Wnf3uIqQ4GxQxTuiLD5wn7MXKgs6Y++31LvkaHSnprnWKkd9 +Cxnb7Ve/GlDEqXgYOpjQLiQiNxUk9KRasZDTeElg5vxfHVxGpgxyROit6egokiR8 +JNglP+zrHicuvzl4f2DGQmjF5C4HQgyFIVrBv+vNKlqKmg2HatKIqBE60u7uw6CI +/Q8ecqU9oVZscOF9T6hUJX0xijGSsE5UTR87U2zo0+fQRJ5qZ5pLjIuF5YiCi0eT +qZex+h1TuTHNFoc1gcAyj5eDZQwwzZiR/Lae/pDyYwKBgQDVPy/elkUkoDm/OlbE +g+wZpBAGGw2ACCK5SpgwRVmHEZUvBbNZgLfE0BydaK88eDTDbLpvfVNvMoNrZQTh +bAqt9XsFBzmfMusiDYbK3ebhooObEcc6I1GmU5fqHuDv3h0CaEli2JjPmB44mXr8 +EGtl98nijNgAmbaA3p7IlWGeGwKBgQC5oPJOjSw6ZRywZwB220tIm0vMcp60sTmq +H+ESaNMkpB38T9OBTdprBjAUzDCZC4jKFwU03ZQtGIN9S6BhogqyP8BRI/+lGJRQ +ZLT1eVSHuC8bX1kVfb5tlahRQ2VUSj5cZxyOiZ8JXa1r1bf8/FZ/0tzLB4a7Ge5Z +2lyQXa3SzwKBgCGCDkmRn0fEDY7o4d17RUw6JXJwKczmel5XRFbBbvH0Z1a+NJJp +0XaRpQ1u96ou0Uur+Bewv72HWHM1qnCpg3wWSMBfhERpwdzV90pFWBQ4bymcv4t5 +JUlXdVWKiJnocvJ/5JgtpMVqB8WpCFQ3WEjriMOakg52GOFjGdw27OHlAoGBAKmk +eezpvXK8dySLbXQx4zI+ol38nie6E13zdmixnczNo42zkjKIaMUISaaoGP20+dTe +huaSXVl9HqXCGJdBVI8kDejZgkdqGBkEgBAaSvMhkwNr9ujaGs7hR4rEkfUfSLB/ +lyx4fvw7PULgdR3hqld06E0v2qRhBV/eXFufET0nAoGAaXKncm7o2LnMIcJV3qrk +c0darLx+VWg0ILnE3/6kGSgGGnMm93tEwSX8h7Nq7QRNkVLUalVAcjm7jR9XGNsY +W8oXqsyqmntNSA2f9w4MqYO2/i/xtN830qW2nw/RLLV5avD6EkY2KrlFbrrZQr2y +q8oFyEAxJc8Kmo5ocnrPq1s= +-----END PRIVATE KEY----- diff --git a/tests/mock_nginx/legacy.conf b/tests/mock_nginx/legacy.conf new file mode 100644 index 0000000..6aee978 --- /dev/null +++ b/tests/mock_nginx/legacy.conf @@ -0,0 +1,26 @@ +events {} + +http { + server { + listen 80; + server_name _; + + location / { + add_header X-Mock-Service "legacy"; + return 200 "Mock Nginx Legacy HTTP\n"; + } + } + + server { + listen 443 ssl; + server_name _; + + ssl_certificate /etc/nginx/certs/server.crt; + ssl_certificate_key /etc/nginx/certs/server.key; + + location / { + add_header X-Mock-Service "legacy"; + return 200 "Mock Nginx Legacy HTTPS\n"; + } + } +} diff --git a/tests/mock_nginx/nginx.conf b/tests/mock_nginx/nginx.conf new file mode 100644 index 0000000..fa46a51 --- /dev/null +++ b/tests/mock_nginx/nginx.conf @@ -0,0 +1,26 @@ +events {} + +http { + server { + listen 80; + server_name _; + + location / { + add_header X-Mock-Service "true"; + return 200 "Mock Nginx HTTP\n"; + } + } + + server { + listen 443 ssl; + server_name _; + + ssl_certificate /etc/nginx/certs/server.crt; + ssl_certificate_key /etc/nginx/certs/server.key; + + location / { + add_header X-Mock-Service "true"; + return 200 "Mock Nginx HTTPS\n"; + } + } +} diff --git a/tests/mock_nginx/staging.conf b/tests/mock_nginx/staging.conf new file mode 100644 index 0000000..db087c1 --- /dev/null +++ b/tests/mock_nginx/staging.conf @@ -0,0 +1,26 @@ +events {} + +http { + server { + listen 8080; + server_name _; + + location / { + add_header X-Mock-Service "staging"; + return 200 "Mock Nginx Staging HTTP\n"; + } + } + + server { + listen 445 ssl; + server_name _; + + ssl_certificate /etc/nginx/certs/server.crt; + ssl_certificate_key /etc/nginx/certs/server.key; + + location / { + add_header X-Mock-Service "staging"; + return 200 "Mock Nginx Staging HTTPS\n"; + } + } +}