CGI-ACL
view release on metacpan or search on metacpan
lib/CGI/ACL.pm view on Meta::CPAN
my $lingua = CGI::Lingua->new(supported => ['en']);
my $acl = CGI::ACL->new()
->deny_cloud() # block AWS, GCP, Azure, etc.
->allow_ip('127.0.0.1') # always allow localhost
->allow_ip('198.51.100.0/24') # corporate network
->deny_all_countries() # default-deny all countries...
->allow_country('GB') # ...except UK
->allow_country('US'); # ...and US
if ($acl->all_denied(lingua => $lingua)) {
print "Content-Type: text/plain\n\n";
print "Access denied.\n";
exit;
}
=head2 Sharing a base ACL across routes with cloning
Call C<new()> on an existing object to get an independent copy.
Changing the copy does not affect the original.
use CGI::ACL;
# Shared base: block cloud for all routes
my $base_acl = CGI::ACL->new()->deny_cloud();
# Admin route: additionally restrict to a single IP
my $admin_acl = $base_acl->new()->allow_ip('198.51.100.1');
if ($admin_acl->all_denied()) {
print "Content-Type: text/plain\n\n";
print "Admin access denied.\n";
exit;
}
The module optionally integrates with L<CGI::Lingua> for country detection.
Runtime configuration is supported via L<Object::Configure>.
=head1 COMMON PITFALLS
The following mistakes are easy to make. Read this section before filing
a bug report.
=head2 allow_country alone has no effect
C<allow_country()> only restricts access when default-deny mode is active.
Default-deny mode is activated by C<deny_country('*')> or
C<deny_all_countries()>. Without it, C<allow_country()> is silently
ignored and everyone is still allowed.
# WRONG -- this allows everyone; allow_country is ignored
my $acl = CGI::ACL->new()->allow_country('US');
# RIGHT -- deny all countries first, then add permitted ones
my $acl = CGI::ACL->new()->deny_all_countries()->allow_country('US');
=head2 deny_cloud overrides allow_ip
Cloud detection has the highest priority. An IP address that is listed
in C<allow_ip()> is still blocked if its reverse DNS resolves to a cloud
provider hostname. This is intentional: cloud IPs can be reassigned, so
the rDNS check is more reliable than the IP address alone.
# This STILL blocks the IP if it is a cloud host
my $acl = CGI::ACL->new()
->deny_cloud()
->allow_ip('198.51.100.5'); # blocked if rDNS says EC2
=head2 Localhost is not automatically allowed
Once any restriction is set, C<127.0.0.1> is subject to the same rules
as any other address. If you need to allow local access (for example,
a health-check endpoint), add it explicitly.
my $acl = CGI::ACL->new()
->allow_ip('127.0.0.1') # must be explicit
->deny_all_countries()
->allow_country('US');
=head2 Forgetting the lingua argument
When country restrictions are active and C<all_denied()> is called without
a C<lingua> argument, the module emits a C<carp> warning and denies the
request. Always pass a C<CGI::Lingua> object when country rules are in use.
# WRONG -- will carp and deny every request
my $acl = CGI::ACL->new()->deny_all_countries()->allow_country('US');
$acl->all_denied();
# RIGHT
my $lingua = CGI::Lingua->new(supported => ['en']);
$acl->all_denied(lingua => $lingua);
=head2 VPN and proxy users bypass IP and country checks
A visitor who connects through a VPN, Tor exit node, or anonymous proxy
will appear to come from the proxy's IP address and country, not their
own. CGI::ACL has no way to detect this. Cloud blocking provides some
mitigation for VPS-based proxies.
=head2 Country codes are case-insensitive but stored lowercase
C<deny_country('BR')> and C<deny_country('br')> are equivalent. All
country codes are stored in lowercase. C<CGI::Lingua::country()> may
return either case; C<all_denied()> normalises it with C<lc()> before
comparing.
=head2 The DNS result cache is not shared between CGI requests
In traditional CGI (one process per request), the per-object DNS cache
is destroyed at the end of every request. The cache is only useful in
persistent-process setups such as FastCGI, mod_perl, or Plack servers,
where the same C<CGI::ACL> object survives across many requests.
=head1 SUBROUTINES/METHODS
=head2 new
Creates and returns a new CGI::ACL object.
When called on an existing object it returns a deep clone of that object,
optionally overriding public fields with the supplied arguments. The public
data hashes (C<allowed_ips>, C<deny_countries>, C<allow_countries>) are
copied so that mutations to the clone do not affect the original.
Derived/private keys (C<_cidrlist>, C<_cloud_cache>) are always cleared;
they are rebuilt from the cloned public state on the next C<all_denied()>
call.
( run in 1.252 second using v1.01-cache-2.11-cpan-800906f7e73 )