package unit; import java.util.*; /** * HttpdProxyNoproxyTest — CWE-407 model test for Apache httpd httpd-0002. * * Models the defect in mod_proxy.c where set_proxy_exclude and * set_proxy_dirconn each scan the full existing array for duplicates before * inserting a new entry. With N directives the total cost is O(N²). * * Defective: ArrayList scanned with strcasecmp per directive → O(N²) total * Fixed: HashSet membership check per directive → O(N) total * * Parameters: * N = 500 directives (e.g. NoProxy entries in httpd.conf) * * Tests: * 1. testDefectiveCorrectness — dedup produces correct unique set * 2. testFixedCorrectness — dedup produces correct unique set * 3. testSlowOpCount — O(N²) comparisons measured * 4. testFastOpCount — O(N) comparisons measured * 5. testRatio — ratio >= 5x (expect ~249x at N=500) */ public class HttpdProxyNoproxyTest { static final int N = 500; // number of NoProxy directives to load // ----------------------------------------------------------------------- // Instrumentation // ----------------------------------------------------------------------- static long slowOps = 0; static long fastOps = 0; static boolean instrumentedStrcasecmp(String a, String b) { slowOps++; return a.equalsIgnoreCase(b); } // ----------------------------------------------------------------------- // Model: defective implementation (APR array + linear dedup) // ----------------------------------------------------------------------- static class DefectiveNoproxyConfig { final List entries = new ArrayList<>(); /** add() models set_proxy_exclude: linear scan then insert. */ void add(String host) { for (int i = 0; i < entries.size(); i++) { if (instrumentedStrcasecmp(entries.get(i), host)) { return; // duplicate, skip } } entries.add(host.toLowerCase(Locale.ROOT)); } } // ----------------------------------------------------------------------- // Model: fixed implementation (hash set for dedup) // ----------------------------------------------------------------------- static class FixedNoproxyConfig { final List entries = new ArrayList<>(); final Set seen = new HashSet<>(); /** add() uses a hash set for O(1) dedup. */ void add(String host) { fastOps++; String lower = host.toLowerCase(Locale.ROOT); if (seen.add(lower)) { entries.add(lower); } } } // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- /** * Generate N distinct hostnames + N/2 duplicates interleaved. * Total directives = N + N/2 = 1.5*N; unique entries = N. */ static List buildDirectives() { List dirs = new ArrayList<>(N + N / 2); for (int i = 0; i < N; i++) { dirs.add("host-" + i + ".example.com"); } // interleave N/2 duplicates (every other original entry, uppercased // to exercise case-insensitive comparison) for (int i = 0; i < N / 2; i++) { dirs.add("HOST-" + (i * 2) + ".EXAMPLE.COM"); } return dirs; } // ----------------------------------------------------------------------- // Assert helpers // ----------------------------------------------------------------------- static void assertTrue(String msg, boolean cond) { if (!cond) throw new AssertionError("FAIL: " + msg); } static void assertEquals(String msg, Object expected, Object actual) { if (!Objects.equals(expected, actual)) throw new AssertionError("FAIL: " + msg + " expected=" + expected + " actual=" + actual); } // ----------------------------------------------------------------------- // Test 1: defective correctness // ----------------------------------------------------------------------- static void testDefectiveCorrectness() { List dirs = buildDirectives(); DefectiveNoproxyConfig cfg = new DefectiveNoproxyConfig(); slowOps = 0; for (String d : dirs) cfg.add(d); assertEquals("defective unique count", N, cfg.entries.size()); // Verify no duplicates remain Set unique = new HashSet<>(cfg.entries); assertEquals("defective no duplicate entries", N, unique.size()); System.out.println("[PASS] testDefectiveCorrectness entries=" + cfg.entries.size()); } // ----------------------------------------------------------------------- // Test 2: fixed correctness // ----------------------------------------------------------------------- static void testFixedCorrectness() { List dirs = buildDirectives(); FixedNoproxyConfig cfg = new FixedNoproxyConfig(); fastOps = 0; for (String d : dirs) cfg.add(d); assertEquals("fixed unique count", N, cfg.entries.size()); Set unique = new HashSet<>(cfg.entries); assertEquals("fixed no duplicate entries", N, unique.size()); System.out.println("[PASS] testFixedCorrectness entries=" + cfg.entries.size()); } // ----------------------------------------------------------------------- // Test 3: slow (defective) op count is O(N²) // ----------------------------------------------------------------------- static void testSlowOpCount() { List dirs = buildDirectives(); DefectiveNoproxyConfig cfg = new DefectiveNoproxyConfig(); slowOps = 0; for (String d : dirs) cfg.add(d); long slow = slowOps; // With N unique entries + N/2 duplicates, total comparisons are at // least N*(N-1)/2 (triangular number for unique insertions alone). long minExpected = (long) N * (N - 1) / 2; assertTrue( "slow ops (" + slow + ") >= N*(N-1)/2 (" + minExpected + ")", slow >= minExpected ); System.out.println("[PASS] testSlowOpCount slowOps=" + slow + " minExpected=" + minExpected); } // ----------------------------------------------------------------------- // Test 4: fast (fixed) op count is O(N) // ----------------------------------------------------------------------- static void testFastOpCount() { List dirs = buildDirectives(); FixedNoproxyConfig cfg = new FixedNoproxyConfig(); fastOps = 0; for (String d : dirs) cfg.add(d); long fast = fastOps; // Every directive costs exactly 1 hash probe (our fastOps counter), // so fastOps == total directives = N + N/2. long totalDirs = N + (long) N / 2; assertEquals("fast ops equals total directive count", totalDirs, fast); System.out.println("[PASS] testFastOpCount fastOps=" + fast); } // ----------------------------------------------------------------------- // Test 5: ratio >= 5x (actual is ~249x at N=500) // ----------------------------------------------------------------------- static void testRatio() { List dirs = buildDirectives(); DefectiveNoproxyConfig defCfg = new DefectiveNoproxyConfig(); slowOps = 0; for (String d : dirs) defCfg.add(d); long slow = slowOps; FixedNoproxyConfig fixCfg = new FixedNoproxyConfig(); fastOps = 0; for (String d : dirs) fixCfg.add(d); long fast = fastOps; double ratio = (double) slow / fast; System.out.printf("[INFO] slowOps=%d fastOps=%d ratio=%.1fx%n", slow, fast, ratio); assertTrue( "ratio (" + ratio + ") >= 5.0x (N=" + N + ")", ratio >= 5.0 ); System.out.printf("[PASS] testRatio ratio=%.1fx%n", ratio); } // ----------------------------------------------------------------------- // Main // ----------------------------------------------------------------------- public static void main(String[] args) { System.out.println("=== HttpdProxyNoproxyTest N=" + N + " ==="); testDefectiveCorrectness(); testFixedCorrectness(); testSlowOpCount(); testFastOpCount(); testRatio(); System.out.println("=== ALL TESTS PASSED ==="); } }