X-Forwarded-For parsing and trusted proxy detection for OCaml
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Squashed 'ocaml-xff/' content from commit 2d77e86f git-subtree-split: 2d77e86f91415a68d5cb15bf7218752ba69b1673

+620
+5
.gitignore
··· 1 + _build/ 2 + _opam/ 3 + *.install 4 + .merlin 5 + dune.lock/
+1
.ocamlformat
··· 1 + version = 0.28.1
+15
LICENSE
··· 1 + ISC License 2 + 3 + Copyright (c) 2025 Thomas Gazagnaire 4 + 5 + Permission to use, copy, modify, and/or distribute this software for any 6 + purpose with or without fee is hereby granted, provided that the above 7 + copyright notice and this permission notice appear in all copies. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 + PERFORMANCE OF THIS SOFTWARE.
+28
dune-project
··· 1 + (lang dune 3.0) 2 + 3 + (name xff) 4 + 5 + (generate_opam_files true) 6 + 7 + (source 8 + (github gazagnaire/ocaml-xff)) 9 + 10 + (authors "Thomas Gazagnaire") 11 + 12 + (maintainers "thomas@gazagnaire.org") 13 + 14 + (license ISC) 15 + 16 + (package 17 + (name xff) 18 + (synopsis "X-Forwarded-For parsing and trusted proxy detection") 19 + (description 20 + "Parse X-Forwarded-For headers to extract client IP addresses behind proxies. 21 + Supports trusted proxy validation using CIDR ranges to prevent IP spoofing. 22 + Implements de-facto X-Forwarded-For standard and aligns with RFC 7239.") 23 + (depends 24 + (ocaml (>= 5.0.0)) 25 + dune 26 + ipaddr 27 + fmt 28 + (alcotest :with-test)))
+15
fuzz/dune
··· 1 + ; Crowbar fuzz testing for xff 2 + ; 3 + ; To run: dune exec ocaml-xff/fuzz/fuzz_xff.exe 4 + ; With AFL: afl-fuzz -i fuzz/corpus -o fuzz/findings -- ./_build/default/ocaml-xff/fuzz/fuzz_xff.exe @@ 5 + 6 + (executable 7 + (name fuzz_xff) 8 + (modules fuzz_xff) 9 + (libraries xff ipaddr crowbar)) 10 + 11 + (rule 12 + (alias fuzz) 13 + (deps fuzz_xff.exe) 14 + (action 15 + (run %{exe:fuzz_xff.exe})))
+85
fuzz/fuzz_xff.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (* Crowbar-based fuzz testing for X-Forwarded-For parsing *) 7 + 8 + open Crowbar 9 + 10 + (* Test that parse_xff never crashes on arbitrary input *) 11 + let test_parse_xff_no_crash input = 12 + ignore (Xff.parse_xff input); 13 + check true 14 + 15 + (* Test that client_ip never crashes *) 16 + let test_client_ip_no_crash input = 17 + ignore (Xff.client_ip input); 18 + check true 19 + 20 + (* Test that parse_cidr never crashes *) 21 + let test_parse_cidr_no_crash input = 22 + ignore (Xff.parse_cidr input); 23 + check true 24 + 25 + (* Test get_client_ip with various combinations *) 26 + let test_get_client_ip_no_crash socket_ip xff_header = 27 + let socket = 28 + match Ipaddr.of_string socket_ip with Ok ip -> Some ip | Error _ -> None 29 + in 30 + let xff = if xff_header = "" then None else Some xff_header in 31 + ignore 32 + (Xff.get_client_ip ~socket_ip:socket ~xff_header:xff ~trusted_proxies:None); 33 + check true 34 + 35 + (* Test with comma-separated IP addresses *) 36 + let test_comma_separated ip1 ip2 ip3 = 37 + let xff = ip1 ^ ", " ^ ip2 ^ ", " ^ ip3 in 38 + ignore (Xff.parse_xff xff); 39 + check true 40 + 41 + (* Test IPv4-like addresses *) 42 + let test_ipv4_xff a b c d = 43 + let ipv4 = 44 + Printf.sprintf "%d.%d.%d.%d" 45 + (abs a mod 256) 46 + (abs b mod 256) 47 + (abs c mod 256) 48 + (abs d mod 256) 49 + in 50 + ignore (Xff.parse_xff ipv4); 51 + check true 52 + 53 + (* Test CIDR notation *) 54 + let test_cidr_notation a b c d prefix = 55 + let cidr = 56 + Printf.sprintf "%d.%d.%d.%d/%d" 57 + (abs a mod 256) 58 + (abs b mod 256) 59 + (abs c mod 256) 60 + (abs d mod 256) 61 + (abs prefix mod 33) 62 + in 63 + ignore (Xff.parse_cidr cidr); 64 + check true 65 + 66 + (* Test is_trusted_proxy never crashes *) 67 + let test_is_trusted_proxy ip_str cidr_str = 68 + (match (Ipaddr.of_string ip_str, Xff.parse_cidr cidr_str) with 69 + | Ok ip, Ok prefix -> ignore (Xff.is_trusted_proxy ip [ prefix ]) 70 + | _ -> ()); 71 + check true 72 + 73 + let () = 74 + add_test ~name:"xff: parse_xff no crash" [ bytes ] test_parse_xff_no_crash; 75 + add_test ~name:"xff: client_ip no crash" [ bytes ] test_client_ip_no_crash; 76 + add_test ~name:"xff: parse_cidr no crash" [ bytes ] test_parse_cidr_no_crash; 77 + add_test ~name:"xff: get_client_ip no crash" [ bytes; bytes ] 78 + test_get_client_ip_no_crash; 79 + add_test ~name:"xff: comma separated" [ bytes; bytes; bytes ] 80 + test_comma_separated; 81 + add_test ~name:"xff: ipv4" [ int; int; int; int ] test_ipv4_xff; 82 + add_test ~name:"xff: cidr notation" 83 + [ int; int; int; int; int ] 84 + test_cidr_notation; 85 + add_test ~name:"xff: is_trusted_proxy" [ bytes; bytes ] test_is_trusted_proxy
+4
lib/dune
··· 1 + (library 2 + (name xff) 3 + (public_name xff) 4 + (libraries ipaddr fmt))
+144
lib/xff.ml
··· 1 + (** X-Forwarded-For parsing and trusted proxy detection. 2 + 3 + This module provides utilities for extracting client IP addresses from 4 + X-Forwarded-For headers when running behind reverse proxies, with support 5 + for trusted proxy validation to prevent IP spoofing. 6 + 7 + {2 Background} 8 + 9 + When a web server runs behind a reverse proxy (nginx, HAProxy, cloud load 10 + balancers), the direct TCP connection comes from the proxy, not the client. 11 + Proxies add the original client IP to the X-Forwarded-For header. 12 + 13 + {2 Security} 14 + 15 + Without trusted proxy validation, malicious clients can spoof their IP by 16 + sending a fake X-Forwarded-For header. This module only trusts XFF headers 17 + from connections originating from configured trusted proxy CIDR ranges. 18 + 19 + {2 References} 20 + 21 + - {{:https://datatracker.ietf.org/doc/html/rfc7239} RFC 7239} - Forwarded 22 + HTTP Extension (standardized header) 23 + - X-Forwarded-For is a de-facto standard documented by MDN *) 24 + 25 + (** {1 Types} *) 26 + 27 + type ip = Ipaddr.t 28 + (** An IP address (IPv4 or IPv6). *) 29 + 30 + type prefix = Ipaddr.Prefix.t 31 + (** A CIDR prefix for matching IP ranges. *) 32 + 33 + (** {1 CIDR Parsing} *) 34 + 35 + let parse_cidr s = 36 + match Ipaddr.Prefix.of_string s with 37 + | Ok prefix -> Ok prefix 38 + | Error (`Msg msg) -> Error (`Msg (Fmt.str "Invalid CIDR '%s': %s" s msg)) 39 + 40 + let parse_cidr_exn s = 41 + match parse_cidr s with 42 + | Ok prefix -> prefix 43 + | Error (`Msg msg) -> invalid_arg msg 44 + 45 + (** {1 Trusted Proxy Detection} *) 46 + 47 + let ip_in_prefix ip prefix = Ipaddr.Prefix.mem ip prefix 48 + 49 + let is_trusted_proxy ip trusted_prefixes = 50 + List.exists (fun prefix -> ip_in_prefix ip prefix) trusted_prefixes 51 + 52 + (** {1 X-Forwarded-For Parsing} *) 53 + 54 + let parse_xff xff_value = 55 + String.split_on_char ',' xff_value 56 + |> List.filter_map (fun s -> 57 + let ip_str = String.trim s in 58 + if ip_str = "" then None 59 + else 60 + (* Remove port if present (e.g., "1.2.3.4:5678") *) 61 + let ip_str_no_port = 62 + match String.rindex_opt ip_str ':' with 63 + | Some idx -> 64 + let before_colon = String.sub ip_str 0 idx in 65 + (* Check if this looks like IPv6 (has multiple colons) *) 66 + if String.contains before_colon ':' then ip_str else before_colon 67 + | None -> ip_str 68 + in 69 + Ipaddr.of_string ip_str_no_port |> Result.to_option) 70 + 71 + let client_ip xff_value = 72 + match parse_xff xff_value with first :: _ -> Some first | [] -> None 73 + 74 + (** {1 Client IP Extraction} *) 75 + 76 + let get_client_ip ~socket_ip ~xff_header ~trusted_proxies = 77 + match (socket_ip, trusted_proxies, xff_header) with 78 + | Some socket_ip, Some prefixes, Some xff 79 + when is_trusted_proxy socket_ip prefixes -> ( 80 + (* Connection from trusted proxy - extract from X-Forwarded-For *) 81 + match client_ip xff with 82 + | Some client_ip -> Some client_ip 83 + | None -> 84 + Some socket_ip (* Failed to parse XFF - fall back to socket IP *)) 85 + | Some socket_ip, _, _ -> 86 + (* No trusted proxies, no XFF header, or not from trusted proxy *) 87 + Some socket_ip 88 + | None, _, _ -> None 89 + 90 + let get_client_ip_string ~socket_ip ~xff_header ~trusted_proxies = 91 + match get_client_ip ~socket_ip ~xff_header ~trusted_proxies with 92 + | Some ip -> Fmt.str "%a" Ipaddr.pp ip 93 + | None -> "unknown" 94 + 95 + (** {1 Common Trusted Proxy Ranges} *) 96 + 97 + let private_ranges = 98 + List.filter_map 99 + (fun s -> Result.to_option (parse_cidr s)) 100 + [ 101 + "10.0.0.0/8"; 102 + (* RFC 1918 private *) 103 + "172.16.0.0/12"; 104 + (* RFC 1918 private *) 105 + "192.168.0.0/16"; 106 + (* RFC 1918 private *) 107 + "127.0.0.0/8"; 108 + (* Loopback *) 109 + "::1/128"; 110 + (* IPv6 loopback *) 111 + "fc00::/7"; 112 + (* IPv6 unique local *) 113 + "fe80::/10" (* IPv6 link-local *); 114 + ] 115 + 116 + let cloudflare_ranges = 117 + List.filter_map 118 + (fun s -> Result.to_option (parse_cidr s)) 119 + [ 120 + (* Cloudflare IPv4 ranges - see https://www.cloudflare.com/ips-v4 *) 121 + "173.245.48.0/20"; 122 + "103.21.244.0/22"; 123 + "103.22.200.0/22"; 124 + "103.31.4.0/22"; 125 + "141.101.64.0/18"; 126 + "108.162.192.0/18"; 127 + "190.93.240.0/20"; 128 + "188.114.96.0/20"; 129 + "197.234.240.0/22"; 130 + "198.41.128.0/17"; 131 + "162.158.0.0/15"; 132 + "104.16.0.0/13"; 133 + "104.24.0.0/14"; 134 + "172.64.0.0/13"; 135 + "131.0.72.0/22"; 136 + (* Cloudflare IPv6 ranges - see https://www.cloudflare.com/ips-v6 *) 137 + "2400:cb00::/32"; 138 + "2606:4700::/32"; 139 + "2803:f800::/32"; 140 + "2405:b500::/32"; 141 + "2405:8100::/32"; 142 + "2a06:98c0::/29"; 143 + "2c0f:f248::/32"; 144 + ]
+102
lib/xff.mli
··· 1 + (** X-Forwarded-For parsing and trusted proxy detection. 2 + 3 + This module provides utilities for extracting client IP addresses from 4 + X-Forwarded-For headers when running behind reverse proxies, with support 5 + for trusted proxy validation to prevent IP spoofing. 6 + 7 + {2 Example} 8 + 9 + {[ 10 + (* Configure trusted proxies *) 11 + let trusted = Xff.private_ranges 12 + 13 + (* Extract client IP from request *) 14 + let client_ip = 15 + Xff.get_client_ip_string ~socket_ip:(Some socket_addr) 16 + ~xff_header:(Some "203.0.113.50, 10.0.0.1") 17 + ~trusted_proxies:(Some trusted) 18 + (* Returns "203.0.113.50" if socket_ip is in trusted range *) 19 + ]} 20 + 21 + {2 References} 22 + 23 + - {{:https://datatracker.ietf.org/doc/html/rfc7239} RFC 7239} - Forwarded 24 + HTTP Extension 25 + - X-Forwarded-For is a de-facto standard *) 26 + 27 + (** {1 Types} *) 28 + 29 + type ip = Ipaddr.t 30 + (** An IP address (IPv4 or IPv6). *) 31 + 32 + type prefix = Ipaddr.Prefix.t 33 + (** A CIDR prefix for matching IP ranges. *) 34 + 35 + (** {1 CIDR Parsing} *) 36 + 37 + val parse_cidr : string -> (prefix, [ `Msg of string ]) result 38 + (** [parse_cidr s] parses a CIDR string like ["192.168.0.0/16"] or 39 + ["2001:db8::/32"]. *) 40 + 41 + val parse_cidr_exn : string -> prefix 42 + (** [parse_cidr_exn s] is like {!parse_cidr} but raises [Invalid_argument] on 43 + error. *) 44 + 45 + (** {1 Trusted Proxy Detection} *) 46 + 47 + val ip_in_prefix : ip -> prefix -> bool 48 + (** [ip_in_prefix ip prefix] returns [true] if [ip] is within the CIDR [prefix]. 49 + *) 50 + 51 + val is_trusted_proxy : ip -> prefix list -> bool 52 + (** [is_trusted_proxy ip prefixes] returns [true] if [ip] matches any prefix in 53 + the list. *) 54 + 55 + (** {1 X-Forwarded-For Parsing} *) 56 + 57 + val parse_xff : string -> ip list 58 + (** [parse_xff header] parses an X-Forwarded-For header value and returns all 59 + valid IP addresses. The header format is ["client, proxy1, proxy2, ..."]. 60 + Invalid entries are silently skipped. Port suffixes are stripped. *) 61 + 62 + val client_ip : string -> ip option 63 + (** [client_ip header] extracts the leftmost (original client) IP from an 64 + X-Forwarded-For header. Returns [None] if no valid IP is found. *) 65 + 66 + (** {1 Client IP Extraction} *) 67 + 68 + val get_client_ip : 69 + socket_ip:ip option -> 70 + xff_header:string option -> 71 + trusted_proxies:prefix list option -> 72 + ip option 73 + (** [get_client_ip ~socket_ip ~xff_header ~trusted_proxies] extracts the real 74 + client IP address considering trusted proxy configuration. 75 + 76 + - If [socket_ip] is from a trusted proxy and [xff_header] is present, 77 + returns the client IP from X-Forwarded-For 78 + - Otherwise returns [socket_ip] 79 + - Returns [None] if [socket_ip] is [None] 80 + 81 + This prevents IP spoofing: only connections from trusted proxies can set the 82 + client IP via X-Forwarded-For. *) 83 + 84 + val get_client_ip_string : 85 + socket_ip:ip option -> 86 + xff_header:string option -> 87 + trusted_proxies:prefix list option -> 88 + string 89 + (** Like {!get_client_ip} but returns a string. Returns ["unknown"] if the IP 90 + cannot be determined. *) 91 + 92 + (** {1 Common Trusted Proxy Ranges} *) 93 + 94 + val private_ranges : prefix list 95 + (** RFC 1918 private ranges, loopback, and IPv6 equivalents: 96 + - [10.0.0.0/8], [172.16.0.0/12], [192.168.0.0/16] 97 + - [127.0.0.0/8], [::1/128] 98 + - [fc00::/7], [fe80::/10] *) 99 + 100 + val cloudflare_ranges : prefix list 101 + (** Cloudflare proxy IP ranges (IPv4 and IPv6). See 102 + {{:https://www.cloudflare.com/ips/} Cloudflare IPs}. *)
+3
test/dune
··· 1 + (test 2 + (name test_xff) 3 + (libraries xff alcotest))
+183
test/test_xff.ml
··· 1 + let 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 + 6 + let 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 + 27 + let 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 + 64 + let test_client_ip () = 65 + (* Extract leftmost IP *) 66 + let ip = Xff.client_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.client_ip "" in 74 + Alcotest.(check (option ip_testable)) "empty header" None ip; 75 + 76 + (* Invalid only *) 77 + let ip = Xff.client_ip "not-an-ip" in 78 + Alcotest.(check (option ip_testable)) "invalid only" None ip 79 + 80 + let 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 + 92 + let 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.get_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.get_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.get_client_ip ~socket_ip:(Some proxy_ip) 118 + ~xff_header:(Some "203.0.113.50") ~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.get_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 + 129 + let test_get_client_ip_string () = 130 + let ip = 131 + Xff.get_client_ip_string ~socket_ip:None ~xff_header:None 132 + ~trusted_proxies:None 133 + in 134 + Alcotest.(check string) "unknown for None" "unknown" ip; 135 + 136 + let ip = 137 + Xff.get_client_ip_string 138 + ~socket_ip:(Some (Ipaddr.of_string_exn "203.0.113.50")) 139 + ~xff_header:None ~trusted_proxies:None 140 + in 141 + Alcotest.(check string) "formats IP" "203.0.113.50" ip 142 + 143 + let test_private_ranges () = 144 + let ip_10 = Ipaddr.of_string_exn "10.0.0.1" in 145 + let ip_172 = Ipaddr.of_string_exn "172.16.0.1" in 146 + let ip_192 = Ipaddr.of_string_exn "192.168.1.1" in 147 + let ip_127 = Ipaddr.of_string_exn "127.0.0.1" in 148 + let ip_public = Ipaddr.of_string_exn "8.8.8.8" in 149 + 150 + Alcotest.(check bool) 151 + "10.x is private" true 152 + (Xff.is_trusted_proxy ip_10 Xff.private_ranges); 153 + Alcotest.(check bool) 154 + "172.16.x is private" true 155 + (Xff.is_trusted_proxy ip_172 Xff.private_ranges); 156 + Alcotest.(check bool) 157 + "192.168.x is private" true 158 + (Xff.is_trusted_proxy ip_192 Xff.private_ranges); 159 + Alcotest.(check bool) 160 + "127.x is private" true 161 + (Xff.is_trusted_proxy ip_127 Xff.private_ranges); 162 + Alcotest.(check bool) 163 + "8.8.8.8 is not private" false 164 + (Xff.is_trusted_proxy ip_public Xff.private_ranges) 165 + 166 + let () = 167 + Alcotest.run "xff" 168 + [ 169 + ("cidr", [ Alcotest.test_case "parse and match" `Quick test_parse_cidr ]); 170 + ( "xff", 171 + [ 172 + Alcotest.test_case "parse header" `Quick test_parse_xff; 173 + Alcotest.test_case "client IP extraction" `Quick test_client_ip; 174 + ] ); 175 + ( "trusted_proxy", 176 + [ 177 + Alcotest.test_case "detection" `Quick test_trusted_proxy; 178 + Alcotest.test_case "get_client_ip" `Quick test_get_client_ip; 179 + Alcotest.test_case "get_client_ip_string" `Quick 180 + test_get_client_ip_string; 181 + Alcotest.test_case "private_ranges" `Quick test_private_ranges; 182 + ] ); 183 + ]
+35
xff.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "X-Forwarded-For parsing and trusted proxy detection" 4 + description: """ 5 + Parse X-Forwarded-For headers to extract client IP addresses behind proxies. 6 + Supports trusted proxy validation using CIDR ranges to prevent IP spoofing. 7 + Implements de-facto X-Forwarded-For standard and aligns with RFC 7239.""" 8 + maintainer: ["thomas@gazagnaire.org"] 9 + authors: ["Thomas Gazagnaire"] 10 + license: "ISC" 11 + homepage: "https://github.com/gazagnaire/ocaml-xff" 12 + bug-reports: "https://github.com/gazagnaire/ocaml-xff/issues" 13 + depends: [ 14 + "ocaml" {>= "5.0.0"} 15 + "dune" {>= "3.0"} 16 + "ipaddr" 17 + "fmt" 18 + "alcotest" {with-test} 19 + "odoc" {with-doc} 20 + ] 21 + build: [ 22 + ["dune" "subst"] {dev} 23 + [ 24 + "dune" 25 + "build" 26 + "-p" 27 + name 28 + "-j" 29 + jobs 30 + "@install" 31 + "@runtest" {with-test} 32 + "@doc" {with-doc} 33 + ] 34 + ] 35 + dev-repo: "git+https://github.com/gazagnaire/ocaml-xff.git"