Revision history for CGI-ACL

0.09	Sun Aug  2 08:54:20 PM EDT 2026
	[ Performance ]
	- Skip DNS lookup entirely in _is_cloud_host() for private, loopback, and
	  link-local addresses (IPv4: 127/8, 10/8, 172.16/12, 192.168/16, 169.254/16;
	  IPv6: ::1, fc00::/7, fe80::/10) — these can never resolve to cloud-provider
	  hostnames
	- Cache per-IP cloud-lookup results in all_denied() with a 300-second TTL;
	  repeated requests from the same IP skip both DNS round-trips entirely.
	  DNS errors and timeouts are not cached so the next request retries.
	  Cache is cleared when an object is cloned via new()
	[ Testing ]
	- Bump minimum version of Test::Mockingbird

0.08	Sat Jun  6 21:26 EDT 2026
	[ Bug Fixes ]
	- Fixed REMOTE_ADDR || DEFAULT_ADDR using || instead of //, which silently
	  substituted 127.0.0.1 for falsy values "0" and "" — a security bypass if
	  loopback was in the allow-list
	- Fixed deny_country(country => []) and allow_country(country => []) creating
	  an empty hashref instead of being a no-op, which tripped the early-return
	  guard and caused all traffic to require a lingua argument
	- Fixed _set_countries() storing "" as a country key when an arrayref contained
	  undef elements; undef elements are now silently skipped
	- Fixed Net::CIDR::cidradd/cidrlookup dying on non-IP strings in the allow-list
	  (e.g. injection attempts); CIDR operations are now wrapped in eval
	- Fixed all_denied() crashing with "Can't call method on non-ref" when a
	  plain string was passed as the lingua argument; now carps and denies
	- Fixed all_denied() dying when the lingua object lacks a country() method;
	  the country() call is now wrapped in eval and missing-method is treated as
	  unknown country (deny)
	- Fixed all_denied() propagating exceptions from _is_cloud_host() / DNS
	  failures to the caller; cloud check now wraps _is_cloud_host in eval and
	  fails safe (treat as non-cloud) when DNS throws
	- Fixed deny_cloud() being bypassed when used without allow_ip or deny_country
	- Fixed deny_country('*') with no allow_country incorrectly allowing all traffic
	- Fixed allow_ip() emitting duplicate warnings when passed a reference
	- Fixed allow_countries not included in all_denied() early-exit guard, causing
	  allow_country-only ACLs to bypass all country checks
	- Fixed auto-vivification of deny_countries when only deny_cloud and allow_country
	  are set, which corrupted object state on subsequent calls
	- Fixed SIGALRM race in _verified_rdns() where alarm(0) called outside eval could
	  let a late-firing alarm kill the CGI process; alarm(0) now also called inside eval
	- Fixed new() to restore croak on bad arguments; soft-carp+undef caused opaque
	  method-chain crashes for all callers
	- Fixed new() plain-function call (CGI::ACL::new()) to always carp and return undef;
	  previously the no-argument case silently constructed an object instead of warning
	- Removed unreachable duplicate AWS hostname pattern in _is_cloud_host()
	- Fixed _verified_rdns() ignoring IPv6 addresses due to inet_aton being IPv4-only;
	  IPv6 clients were bypassing deny_cloud entirely
	- Fixed new() on an existing object (clone mode) using a shallow copy of nested
	  hashrefs, causing mutations to deny_countries/allow_countries/allowed_ips on
	  a clone to also modify the original object

	[ Enhancements ]
	- Cache CIDR list in all_denied() to avoid rebuilding it on every call
	- Add 10-second timeout on DNS lookups in _verified_rdns() on non-Windows platforms
	- Added Test Dashboard at https://nigelhorne.github.io/CGI-ACL/coverage/

	[ Documentation ]
	- Document that deny_cloud() takes precedence over allow_ip()

	[ Critique refactoring ]
	- Fixed deny_country() returning undef instead of $self on bad-ref argument (broke method chaining)
	- Fixed deny_country() carp message incorrectly referencing $ip_address instead of $country
	- Fixed allow_ip() and allow_country() returning undef instead of $self on bad-ref argument
	- Replaced magic number 10 (DNS timeout), '*' wildcard, and '127.0.0.1' with Readonly named constants
	- Replaced ten individual cloud-pattern return statements in _is_cloud_host() with a Readonly @CLOUD_PATTERNS list
	- Extracted _set_countries() private helper to eliminate duplicated code between deny_country/allow_country
	- Added use autodie qw(:all) for safer built-in error handling
	- Added comprehensive POD for all public methods: FORMAL SPECIFICATION (Z calculus), API SPECIFICATION, MESSAGES table
	- Added purpose/entry/exit/side-effects comments for all private routines

	[ Tests ]
	- Migrated t/deny_cloud.t from Test::MockModule to Test::Mockingbird
	- Added IPv6 deny_cloud tests to t/deny_cloud.t
	- Added chaining tests: verify deny_country/allow_country/allow_ip return $self on bad-ref args
	- Added t/function.t: white-box function-level tests for all public and private functions,
	  including mocked DNS helpers, $_ clobber checks, and memory cycle checks
	- Fixed new() to carp and return undef for all plain-function calls (CGI::ACL::new()),
	  including the no-argument case that previously fell through to create an object silently

0.07	Thu Apr 16 19:38:57 EDT 2026
	Fixed call to _verified_rdns
	Allow an object to be configured at runtime via Object::Configure

0.06	Wed Mar  4 06:33:13 EST 2026
	Added t/30-basics.t
	Use Test::Needs
	Use Test::DescribeMe to simplify tests
	Use gtar to create a distribution on Macs
	Check that REMOTE_ADDR is a sane IP address
	Added deny_cloud

0.05	Tue Apr  2 16:26:14 EDT 2024
	Calling new on an object now returns a clone rather than setting the defaults in the new object

0.04	Fri May 21 14:54:04 EDT 2021
	Do something sensible if the remote country can't be determined
	By default, localhost is not allowed access

0.03	Fri Dec  7 11:19:53 EST 2018
	Added allow_country and deny_country('*')

0.02	Tue Feb 21 11:11:32 EST 2017
	Fixed t/country.t

0.01	Wed Feb 15 15:52:08 EST 2017
        First draft
