File Coverage

blib/lib/AWS/IP.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1 1     1   832 use strict;
  1         3  
  1         31  
2 1     1   5 use warnings;
  1         3  
  1         82  
3             package AWS::IP;
4 1     1   560 use Cache::File;
  0            
  0            
5             use Carp;
6             use HTTP::Tiny;
7             use JSON::XS;
8             use File::Temp 'tempdir';
9             use Net::CIDR::Set;
10              
11             # required by HTTP::Tiny for https
12             use IO::Socket::SSL 1.56;
13             use Net::SSLeay 1.49;
14              
15             use constant CACHE_KEY => 'AWS_IPS';
16              
17             our $VERSION = 0.04;
18              
19             # ABSTRACT: Get and search AWS IP ranges in a caching, auto-refreshing way
20              
21             =head2 SYNOPSIS
22              
23             use AWS::IP;
24              
25             my $aws = AWS::IP->new(600, '/tmp/aws_ip_cache');
26              
27             # get the raw data as a Perl reference
28             my $aws_ip_data = $aws->get_raw_data;
29              
30             # check if an ip address is AWS
31             if ($aws->is_aws_ip('50.0.0.1')
32             {
33             ..
34             }
35              
36             # get a list of all AWS cidrs
37             my $cidrs = $aws->get_cidrs;
38              
39             for (@$cidrs)
40             {
41             ...
42             }
43              
44             # create your own ip checks
45             use Net::CIDR::Set;
46              
47             my $ec2_cidrs = $aws->get_cidrs_by_service('EC2');
48             my $aws_ec2_set = Net::CIDR::Set->new( @$ec2_cidrs );
49              
50             if ($aws_ec2_set->contains($ip)
51             {
52             ...
53             }
54              
55             # time passes, cache has expired
56             $aws_ip_data = $aws->get_raw_data; # auto refreshes
57              
58             =head2 DESCRIPTION
59              
60             AWS L their IP ranges, which periodically change. This module downloads and serializes the IP ranges into a Perl data hash reference. It caches the data, and if the cache expires, re-downloads a new version. This can be helpful if you want to block all AWS IP addresses and periodically refresh the blocked IPs.
61              
62             =head2 new ($cache_timeout_secs, [$cache_path])
63              
64             Creates a new AWS::IP object and sets up the cache. Requires an number for the cache timeout seconds. Optionally takes a cache path argument. If no cache path is supplied, AWS::IP will use a random temp directory. If you want to reuse the cache over multiple processes, provide a cache path.
65              
66             =cut
67              
68             sub new
69             {
70             croak 'Incorrect number of args passed to AWS::IP->new()' unless @_ >= 2 && @_ <= 3;
71             my ($class, $cache_timeout_secs, $cache_path) = @_;
72              
73             # validate args
74             unless ($cache_timeout_secs
75             && $cache_timeout_secs =~ /^[0-9]+$/)
76             {
77             croak 'Error argument cache_timeout_secs must be a positive integer';
78             }
79              
80             bless {
81             cache => Cache::File->new( cache_root => ($cache_path || tempdir()),
82             lock_level => Cache::File::LOCK_LOCAL(),
83             default_expires => "$cache_timeout_secs sec"),
84             }, $class;
85             }
86              
87             =head2 ip_is_aws ($ip, [$service])
88              
89             Boolean method to test if an ip address is from AWS. Optionally takes a service name (AMAZON|EC2|CLOUDFRONT|ROUTE53|ROUTE53_HEALTHCHECKS) and restricts the check to AWS ip addresses for that service.
90              
91             If you are checking more than one ip address, it's more efficient to pull the CIDRs you want, then use L to test if the ips are present in the CIDRs (see example in SYNOPSIS).
92              
93             =cut
94              
95             sub ip_is_aws
96             {
97             my ($self, $ip, $service) = @_;
98              
99             croak 'Error must supply an ip address' unless $ip;
100              
101             my $ip_ranges;
102              
103             if ($service)
104             {
105             $ip_ranges = Net::CIDR::Set->new( map { $_->{ip_prefix} } grep { $_->{service} eq $service } @{$self->get_raw_data->{prefixes}});
106             }
107             else
108             {
109             $ip_ranges = Net::CIDR::Set->new( map { $_->{ip_prefix} } @{$self->get_raw_data->{prefixes}} );
110             }
111             $ip_ranges->contains($ip);
112             }
113              
114              
115             =head2 get_raw_data
116              
117             Returns the entire raw IP dataset as a Perl data structure.
118              
119             =cut
120              
121             sub get_raw_data
122             {
123             my ($self) = @_;
124              
125             my $entry = $self->{cache}->entry(CACHE_KEY);
126              
127             if ($entry->exists)
128             {
129             decode_json($entry->get());
130             }
131             else
132             {
133             decode_json($self->_refresh_cache);
134             }
135             }
136              
137             =head2 get_cidrs
138              
139             Returns an arrayref of the L in the AWS IP address data.
140              
141             =cut
142              
143             sub get_cidrs
144             {
145             my ($self) = @_;
146             [ map { $_->{ip_prefix} } @{$self->get_raw_data->{prefixes}} ];
147             }
148              
149             =head2 get_cidrs_by_region ($region)
150              
151             Returns an arrayref of CIDRs matching the provided region.
152              
153             =cut
154              
155             sub get_cidrs_by_region
156             {
157             my ($self, $region) = @_;
158              
159             croak 'Error must provide region' unless $region;
160             [ map { $_->{ip_prefix} } grep { $_->{region} eq $region } @{$self->get_raw_data->{prefixes}} ];
161             }
162              
163             =head2 get_cidrs_by_service ($service)
164              
165             Returns an arrayref of CIDRs matching the provided service (AMAZON|EC2|CLOUDFRONT|ROUTE53|ROUTE53_HEALTHCHECKS).
166              
167             =cut
168              
169             sub get_cidrs_by_service
170             {
171             my ($self, $service) = @_;
172              
173             croak 'Error must provide service' unless $service;
174             [ map { $_->{ip_prefix} } grep { $_->{service} eq $service } @{$self->get_raw_data->{prefixes}} ];
175             }
176              
177             =head2 get_regions
178              
179             Returns an arrayref of the regions in the AWS IP address data.
180              
181             =cut
182              
183             sub get_regions
184             {
185             my ($self) = @_;
186             my %regions;
187             for (@{$self->get_raw_data->{prefixes}})
188             {
189             $regions{ $_->{region} } = 1;
190             }
191             [ keys %regions ];
192             }
193              
194             =head2 get_services
195              
196             Returns an arrayref of the services (Amazon, EC2 etc) in the AWS IP address data.
197              
198             =cut
199              
200             sub get_services
201             {
202             my ($self) = @_;
203             my %services;
204             for (@{$self->get_raw_data->{prefixes}})
205             {
206             $services{ $_->{service} } = 1;
207             }
208             [ keys %services ];
209             }
210              
211             =head2 SEE ALSO
212              
213             L - is similar to this module but does not provide cacheing.
214              
215             Amazon's L on AWS IP ranges.
216              
217             =cut
218              
219              
220             sub _refresh_cache
221             {
222             my ($self) = @_;
223              
224             my $response = HTTP::Tiny->new->get('https://ip-ranges.amazonaws.com/ip-ranges.json');
225              
226             if ($response->{success})
227             {
228             my $entry = $self->{cache}->entry(CACHE_KEY);
229             $entry->set($response->{content});
230              
231             # return the data
232             $response->{content};
233             }
234             else
235             {
236             croak "Error requesting $response->{url} $response->{code} $response->{reason}";
237             }
238             }
239              
240             sub _refresh_cache_from_string
241             {
242             my ($self, $data) = @_;
243              
244             my $entry = $self->{cache}->entry(CACHE_KEY);
245             $entry->set($data);
246              
247             # return the data
248             $data;
249             }
250             1;