diff --git a/examples/docker/docker-compose-cloudflare.yml b/examples/docker/docker-compose-cloudflare.yml index 660dc96..aa0d456 100644 --- a/examples/docker/docker-compose-cloudflare.yml +++ b/examples/docker/docker-compose-cloudflare.yml @@ -66,7 +66,7 @@ services: # Web application behind Cloudflare (header-echo server for testing) webapp: - build: ./python-app + build: ../fixtures/header-echo labels: easyhaproxy.http.host: myapp.local easyhaproxy.http.port: 80 diff --git a/examples/docker/python-app/Dockerfile b/examples/fixtures/header-echo/Dockerfile similarity index 76% rename from examples/docker/python-app/Dockerfile rename to examples/fixtures/header-echo/Dockerfile index b355409..4cedadb 100644 --- a/examples/docker/python-app/Dockerfile +++ b/examples/fixtures/header-echo/Dockerfile @@ -8,4 +8,4 @@ RUN chmod +x server.py EXPOSE 8080 -CMD ["python3", "server.py"] \ No newline at end of file +CMD ["python3", "server.py"] diff --git a/examples/fixtures/header-echo/README.md b/examples/fixtures/header-echo/README.md new file mode 100644 index 0000000..d84bb63 --- /dev/null +++ b/examples/fixtures/header-echo/README.md @@ -0,0 +1,66 @@ +# Header Echo Server - Test Fixture + +A lightweight Python HTTP server that echoes all request headers as JSON. Used for testing HAProxy plugins that manipulate headers and client IPs. + +## Purpose + +This test fixture is used by both Docker Compose and Kubernetes test suites to verify: +- Header manipulation (e.g., X-Forwarded-For, CF-Connecting-IP) +- IP restoration plugins (Cloudflare, custom CDN integrations) +- Request routing and backend visibility + +## Usage + +### Docker Compose +```yaml +services: + webapp: + build: ../fixtures/header-echo + ports: + - "8080:8080" +``` + +### Kubernetes +```bash +# Build and load into kind cluster +docker build -t header-echo-server:test . +kind load docker-image header-echo-server:test --name your-cluster + +# Use in deployment +spec: + containers: + - name: webapp + image: header-echo-server:test + imagePullPolicy: Never +``` + +### Manual Testing +```bash +# Start the server +python3 server.py + +# Test it +curl http://localhost:8080 +# Returns JSON with all headers, client IP, and X-Forwarded-For value +``` + +## Response Format + +```json +{ + "headers": { + "Host": "localhost:8080", + "User-Agent": "curl/7.81.0", + "Accept": "*/*" + }, + "client_ip": "127.0.0.1", + "x_forwarded_for": "NOT SET" +} +``` + +## Used By + +- `examples/docker/docker-compose-cloudflare.yml` +- `examples/docker/test_docker_compose.py::TestCloudflare` +- `examples/kubernetes/cloudflare.yml` +- `examples/kubernetes/test_kubernetes.py::TestCloudflare` diff --git a/examples/docker/python-app/server.py b/examples/fixtures/header-echo/server.py similarity index 97% rename from examples/docker/python-app/server.py rename to examples/fixtures/header-echo/server.py index 4b9e0a4..5bb1695 100644 --- a/examples/docker/python-app/server.py +++ b/examples/fixtures/header-echo/server.py @@ -30,4 +30,4 @@ if __name__ == '__main__': port = 8080 server = HTTPServer(('0.0.0.0', port), HeaderEchoHandler) print(f'Header echo server running on port {port}...') - server.serve_forever() \ No newline at end of file + server.serve_forever() diff --git a/examples/kubernetes/cloudflare.yml b/examples/kubernetes/cloudflare.yml index 84f8c32..f2d14f6 100644 --- a/examples/kubernetes/cloudflare.yml +++ b/examples/kubernetes/cloudflare.yml @@ -48,10 +48,11 @@ # # Test via port-forward # kubectl port-forward -n easyhaproxy deployment/easyhaproxy 8080:80 # curl -H "Host: myapp.example.local" http://localhost:8080 -# # Expected: 200 OK with "App Behind Cloudflare" +# # Expected: 200 OK with JSON response containing headers, client_ip, and x_forwarded_for # -# # In production behind Cloudflare, the plugin will restore real client IPs -# # from the CF-Connecting-IP header +# # Test IP translation with CF-Connecting-IP header +# curl -H "Host: myapp.example.local" -H "CF-Connecting-IP: 1.2.3.4" http://localhost:8080 +# # Expected: x_forwarded_for should be "1.2.3.4" # ``` # # CLEAN UP: @@ -93,12 +94,10 @@ spec: spec: containers: - name: webapp - image: byjg/static-httpserver + image: header-echo-server:test + imagePullPolicy: Never ports: - containerPort: 8080 - env: - - name: TITLE - value: "App Behind Cloudflare" resources: limits: cpu: '0.1' diff --git a/examples/kubernetes/test_kubernetes.py b/examples/kubernetes/test_kubernetes.py index a9a0c47..b1d679f 100644 --- a/examples/kubernetes/test_kubernetes.py +++ b/examples/kubernetes/test_kubernetes.py @@ -840,9 +840,28 @@ def k8s_jwt_validator_secret(kind_cluster) -> Generator[dict, None, None]: @pytest.fixture -def k8s_cloudflare(kind_cluster) -> Generator[str, None, None]: +def k8s_cloudflare(kind_cluster, kind_cmd) -> Generator[str, None, None]: """Fixture for cloudflare.yml with base64-encoded IP list""" kubectl_cmd = kind_cluster["kubectl"] + cluster_name = kind_cluster["name"] + + # Build header-echo server image locally + header_echo_dir = BASE_DIR.parent / "fixtures" / "header-echo" + print(" → Building header-echo-server:test image...") + subprocess.run( + ["docker", "build", "-t", "header-echo-server:test", str(header_echo_dir)], + check=True, + capture_output=True + ) + + # Load image into kind cluster + print(" → Loading header-echo-server:test into kind cluster...") + subprocess.run( + [kind_cmd, "load", "docker-image", "header-echo-server:test", + "--name", cluster_name], + check=True, + capture_output=True + ) # Create a modified cloudflare manifest with base64-encoded test IPs # Include 127.0.0.1 and Docker/kind network ranges so test requests work @@ -1881,7 +1900,7 @@ class TestCloudflare: "Built-in Cloudflare IP found (ip_list should take precedence)" def test_access_to_webapp(self, k8s_cloudflare): - """Test that the webapp is accessible via the Cloudflare ingress""" + """Test that the webapp is accessible and returns JSON""" kubectl = k8s_cloudflare # Wait for EasyHAProxy to discover and configure the ingress @@ -1898,8 +1917,49 @@ class TestCloudflare: ) assert result.returncode == 0, f"Curl failed with return code {result.returncode}" - assert "App Behind Cloudflare" in result.stdout, \ - f"Expected 'App Behind Cloudflare' in response, got: {result.stdout}" + + # Parse JSON response + data = json.loads(result.stdout) + + # Verify JSON structure + assert "headers" in data, "Response should contain 'headers' field" + assert "client_ip" in data, "Response should contain 'client_ip' field" + assert "x_forwarded_for" in data, "Response should contain 'x_forwarded_for' field" + + def test_cloudflare_ip_translation_works(self, k8s_cloudflare): + """Test that Cloudflare plugin actually translates CF-Connecting-IP to X-Forwarded-For""" + kubectl = k8s_cloudflare + + # Wait for EasyHAProxy to be ready + assert wait_for_easyhaproxy_discovery(kubectl, "myapp.example.local", timeout=30), \ + "EasyHAProxy did not become ready within 30 seconds" + + # Send request with CF-Connecting-IP header + test_ip = "203.0.113.50" + result = subprocess.run( + ["curl", "-s", + "-H", "Host: myapp.example.local", + "-H", f"CF-Connecting-IP: {test_ip}", + f"http://localhost:{HTTP_PORT}"], + capture_output=True, + text=True, + timeout=10 + ) + + assert result.returncode == 0, f"Curl failed" + + # Parse JSON response from header-echo server + data = json.loads(result.stdout) + + # VERIFY: X-Forwarded-For was set to the CF-Connecting-IP value + # This proves the Cloudflare plugin actually works, not just that config exists + assert data['x_forwarded_for'] == test_ip, \ + f"Expected X-Forwarded-For to be '{test_ip}' (from CF-Connecting-IP), " \ + f"got '{data['x_forwarded_for']}'. Cloudflare IP translation NOT working!" + + # Verify client_ip is still the HAProxy/ingress IP (connection doesn't change) + assert data['client_ip'] != test_ip, \ + f"client_ip should be HAProxy pod IP, not the translated IP" # =============================================================================