X-Forwarded-For parsing and trusted proxy detection for OCaml
at main 175 lines 6.0 kB view raw
1let ip_testable = 2 Alcotest.testable 3 (fun ppf ip -> Fmt.pf ppf "%a" Ipaddr.pp ip) 4 (fun a b -> Ipaddr.compare a b = 0) 5 6let test_parse_cidr () = 7 (* Valid CIDR *) 8 let prefix = Xff.parse_cidr_exn "192.168.0.0/16" in 9 Alcotest.(check bool) 10 "192.168.1.1 in 192.168.0.0/16" true 11 (Xff.ip_in_prefix (Ipaddr.of_string_exn "192.168.1.1") prefix); 12 Alcotest.(check bool) 13 "10.0.0.1 not in 192.168.0.0/16" false 14 (Xff.ip_in_prefix (Ipaddr.of_string_exn "10.0.0.1") prefix); 15 16 (* IPv6 CIDR *) 17 let prefix6 = Xff.parse_cidr_exn "2001:db8::/32" in 18 Alcotest.(check bool) 19 "2001:db8::1 in 2001:db8::/32" true 20 (Xff.ip_in_prefix (Ipaddr.of_string_exn "2001:db8::1") prefix6); 21 22 (* Invalid CIDR *) 23 match Xff.parse_cidr "not-a-cidr" with 24 | Error _ -> () 25 | Ok _ -> Alcotest.fail "Expected error for invalid CIDR" 26 27let test_parse_xff () = 28 (* Single IP *) 29 let ips = Xff.parse_xff "203.0.113.50" in 30 Alcotest.(check int) "single IP count" 1 (List.length ips); 31 Alcotest.(check ip_testable) 32 "single IP value" 33 (Ipaddr.of_string_exn "203.0.113.50") 34 (List.hd ips); 35 36 (* Multiple IPs *) 37 let ips = Xff.parse_xff "203.0.113.50, 70.41.3.18, 150.172.238.178" in 38 Alcotest.(check int) "multiple IPs count" 3 (List.length ips); 39 Alcotest.(check ip_testable) 40 "first IP" 41 (Ipaddr.of_string_exn "203.0.113.50") 42 (List.hd ips); 43 44 (* With port suffix *) 45 let ips = Xff.parse_xff "203.0.113.50:8080" in 46 Alcotest.(check int) "IP with port count" 1 (List.length ips); 47 Alcotest.(check ip_testable) 48 "IP without port" 49 (Ipaddr.of_string_exn "203.0.113.50") 50 (List.hd ips); 51 52 (* IPv6 *) 53 let ips = Xff.parse_xff "2001:db8::1, 203.0.113.50" in 54 Alcotest.(check int) "mixed IP count" 2 (List.length ips); 55 Alcotest.(check ip_testable) 56 "IPv6 first" 57 (Ipaddr.of_string_exn "2001:db8::1") 58 (List.hd ips); 59 60 (* Empty and invalid entries *) 61 let ips = Xff.parse_xff "203.0.113.50, , invalid, 10.0.0.1" in 62 Alcotest.(check int) "skip invalid entries" 2 (List.length ips) 63 64let test_client_ip () = 65 (* Extract leftmost IP *) 66 let ip = Xff.first_xff_ip "203.0.113.50, 10.0.0.1, 10.0.0.2" in 67 Alcotest.(check (option ip_testable)) 68 "leftmost IP" 69 (Some (Ipaddr.of_string_exn "203.0.113.50")) 70 ip; 71 72 (* Empty header *) 73 let ip = Xff.first_xff_ip "" in 74 Alcotest.(check (option ip_testable)) "empty header" None ip; 75 76 (* Invalid only *) 77 let ip = Xff.first_xff_ip "not-an-ip" in 78 Alcotest.(check (option ip_testable)) "invalid only" None ip 79 80let test_trusted_proxy () = 81 let trusted = [ Xff.parse_cidr_exn "10.0.0.0/8" ] in 82 let proxy_ip = Ipaddr.of_string_exn "10.0.0.1" in 83 let untrusted_ip = Ipaddr.of_string_exn "203.0.113.1" in 84 85 Alcotest.(check bool) 86 "proxy is trusted" true 87 (Xff.is_trusted_proxy proxy_ip trusted); 88 Alcotest.(check bool) 89 "external is not trusted" false 90 (Xff.is_trusted_proxy untrusted_ip trusted) 91 92let test_get_client_ip () = 93 let trusted = [ Xff.parse_cidr_exn "10.0.0.0/8" ] in 94 let proxy_ip = Ipaddr.of_string_exn "10.0.0.1" in 95 let client_ip = Ipaddr.of_string_exn "203.0.113.50" in 96 let untrusted_ip = Ipaddr.of_string_exn "198.51.100.1" in 97 98 (* Trusted proxy with XFF - use XFF client *) 99 let ip = 100 Xff.client_ip ~socket_ip:(Some proxy_ip) 101 ~xff_header:(Some "203.0.113.50, 10.0.0.1") 102 ~trusted_proxies:(Some trusted) 103 in 104 Alcotest.(check (option ip_testable)) 105 "trusted proxy uses XFF" (Some client_ip) ip; 106 107 (* Untrusted connection with XFF - ignore XFF, use socket IP *) 108 let ip = 109 Xff.client_ip ~socket_ip:(Some untrusted_ip) 110 ~xff_header:(Some "203.0.113.50") ~trusted_proxies:(Some trusted) 111 in 112 Alcotest.(check (option ip_testable)) 113 "untrusted ignores XFF" (Some untrusted_ip) ip; 114 115 (* No trusted proxies configured - use socket IP *) 116 let ip = 117 Xff.client_ip ~socket_ip:(Some proxy_ip) ~xff_header:(Some "203.0.113.50") 118 ~trusted_proxies:None 119 in 120 Alcotest.(check (option ip_testable)) "no trusted config" (Some proxy_ip) ip; 121 122 (* No XFF header - use socket IP *) 123 let ip = 124 Xff.client_ip ~socket_ip:(Some proxy_ip) ~xff_header:None 125 ~trusted_proxies:(Some trusted) 126 in 127 Alcotest.(check (option ip_testable)) "no XFF header" (Some proxy_ip) ip 128 129let test_get_client_ip_string () = 130 let ip = 131 Xff.client_ip_string ~socket_ip:None ~xff_header:None ~trusted_proxies:None 132 in 133 Alcotest.(check string) "unknown for None" "unknown" ip; 134 135 let ip = 136 Xff.client_ip_string 137 ~socket_ip:(Some (Ipaddr.of_string_exn "203.0.113.50")) 138 ~xff_header:None ~trusted_proxies:None 139 in 140 Alcotest.(check string) "formats IP" "203.0.113.50" ip 141 142let test_private_ranges () = 143 let ip_10 = Ipaddr.of_string_exn "10.0.0.1" in 144 let ip_172 = Ipaddr.of_string_exn "172.16.0.1" in 145 let ip_192 = Ipaddr.of_string_exn "192.168.1.1" in 146 let ip_127 = Ipaddr.of_string_exn "127.0.0.1" in 147 let ip_public = Ipaddr.of_string_exn "8.8.8.8" in 148 149 Alcotest.(check bool) 150 "10.x is private" true 151 (Xff.is_trusted_proxy ip_10 Xff.private_ranges); 152 Alcotest.(check bool) 153 "172.16.x is private" true 154 (Xff.is_trusted_proxy ip_172 Xff.private_ranges); 155 Alcotest.(check bool) 156 "192.168.x is private" true 157 (Xff.is_trusted_proxy ip_192 Xff.private_ranges); 158 Alcotest.(check bool) 159 "127.x is private" true 160 (Xff.is_trusted_proxy ip_127 Xff.private_ranges); 161 Alcotest.(check bool) 162 "8.8.8.8 is not private" false 163 (Xff.is_trusted_proxy ip_public Xff.private_ranges) 164 165let suite = 166 ( "xff", 167 [ 168 Alcotest.test_case "cidr parse and match" `Quick test_parse_cidr; 169 Alcotest.test_case "xff parse header" `Quick test_parse_xff; 170 Alcotest.test_case "xff client ip extraction" `Quick test_client_ip; 171 Alcotest.test_case "trusted_proxy detection" `Quick test_trusted_proxy; 172 Alcotest.test_case "client_ip" `Quick test_get_client_ip; 173 Alcotest.test_case "client_ip_string" `Quick test_get_client_ip_string; 174 Alcotest.test_case "private_ranges" `Quick test_private_ranges; 175 ] )