java-topology/defects/httpd/unit/HttpdProxyNoproxyTest.java

225 lines
8.5 KiB
Java

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<String> 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<String> entries = new ArrayList<>();
final Set<String> 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<String> buildDirectives() {
List<String> 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<String> 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<String> 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<String> dirs = buildDirectives();
FixedNoproxyConfig cfg = new FixedNoproxyConfig();
fastOps = 0;
for (String d : dirs) cfg.add(d);
assertEquals("fixed unique count", N, cfg.entries.size());
Set<String> 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<String> 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<String> 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<String> 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 ===");
}
}