File Coverage

blib/lib/GeoIP2/WebService/Client.pm
Criterion Covered Total %
statement 148 150 98.6
branch 14 14 100.0
condition 6 10 60.0
subroutine 42 43 97.6
pod 3 4 75.0
total 213 221 96.3


line stmt bran cond sub pod time code
1             package GeoIP2::WebService::Client;
2              
3 1     1   137351 use 5.008;
  1         6  
4              
5 1     1   4 use strict;
  1         3  
  1         18  
6 1     1   5 use warnings;
  1         1  
  1         39  
7              
8             our $VERSION = '2.006002';
9              
10 1     1   419 use Moo;
  1         8589  
  1         5  
11              
12 1     1   1623 use Data::Validate::IP 0.25 qw( is_public_ip );
  1         30116  
  1         70  
13 1     1   404 use GeoIP2::Error::Generic;
  1         4  
  1         29  
14 1     1   387 use GeoIP2::Error::HTTP;
  1         2  
  1         28  
15 1     1   371 use GeoIP2::Error::IPAddressNotFound;
  1         2  
  1         28  
16 1     1   318 use GeoIP2::Error::WebService;
  1         2  
  1         27  
17 1     1   322 use GeoIP2::Model::City;
  1         3  
  1         33  
18 1     1   379 use GeoIP2::Model::Country;
  1         3  
  1         30  
19 1     1   373 use GeoIP2::Model::Insights;
  1         3  
  1         33  
20             use GeoIP2::Types
21 1     1   6 qw( JSONObject MaxMindID MaxMindLicenseKey Str URIObject UserAgentObject );
  1         1  
  1         65  
22 1     1   430 use HTTP::Headers;
  1         6349  
  1         29  
23 1     1   371 use HTTP::Request;
  1         5771  
  1         27  
24 1     1   345 use JSON::MaybeXS;
  1         4689  
  1         51  
25 1     1   390 use MIME::Base64 qw( encode_base64 );
  1         509  
  1         69  
26 1     1   357 use LWP::Protocol::https;
  1         77611  
  1         45  
27 1     1   586 use LWP::UserAgent;
  1         10153  
  1         32  
28 1     1   409 use Params::Validate qw( validate );
  1         2375  
  1         56  
29 1     1   5 use Scalar::Util qw( blessed );
  1         2  
  1         35  
30 1     1   5 use Sub::Quote qw( quote_sub );
  1         2  
  1         38  
31 1     1   4 use Try::Tiny;
  1         2  
  1         35  
32 1     1   6 use URI;
  1         1  
  1         22  
33              
34 1     1   5 use namespace::clean -except => 'meta';
  1         1  
  1         8  
35              
36             with 'GeoIP2::Role::HasLocales';
37              
38             has account_id => (
39             is => 'ro',
40             isa => MaxMindID,
41             required => 1,
42             );
43             *user_id = \&account_id; # for backwards-compatibility
44              
45             has license_key => (
46             is => 'ro',
47             isa => MaxMindLicenseKey,
48             required => 1,
49             );
50              
51             has host => (
52             is => 'ro',
53             isa => Str,
54             default => quote_sub(q{ 'geoip.maxmind.com' }),
55             );
56              
57             has ua => (
58             is => 'ro',
59             isa => UserAgentObject,
60             builder => '_build_ua',
61             );
62              
63             has _base_uri => (
64             is => 'ro',
65             isa => URIObject,
66             lazy => 1,
67             builder => '_build_base_uri',
68             );
69              
70             has _json => (
71             is => 'ro',
72             isa => JSONObject,
73             init_arg => undef,
74             default => quote_sub(q{ JSON::MaybeXS->new(utf8 => 1) }),
75             );
76              
77             around BUILDARGS => sub {
78             my ( $orig, @args ) = @_;
79             my %params = %{ $orig->(@args) };
80             $params{account_id} = delete $params{user_id}
81             if exists $params{user_id};
82             return \%params;
83             };
84              
85             sub BUILD {
86 16     16 0 127 my $self = shift;
87              
88             ## no critic (TryTiny::RequireBlockTermination)
89 16   50 16   71 my $self_version = try { 'v' . $self->VERSION() } || 'v?';
  16         472  
90              
91 16         205 my $ua = $self->ua();
92 16   50 16   51 my $ua_version = try { 'v' . $ua->VERSION() } || 'v?';
  16         297  
93              
94 16         293 my $agent
95             = blessed($self)
96             . " $self_version" . ' ('
97             . blessed($ua) . q{ }
98             . $ua_version . q{ / }
99             . "Perl $^V)";
100              
101 16         57 $ua->agent($agent);
102             }
103              
104             sub country {
105 21     21 1 8586 my $self = shift;
106              
107 21         63 return $self->_response_for(
108             'country',
109             'GeoIP2::Model::Country',
110             @_,
111             );
112             }
113              
114             sub city {
115 0     0 1 0 my $self = shift;
116              
117 0         0 return $self->_response_for(
118             'city',
119             'GeoIP2::Model::City',
120             @_,
121             );
122             }
123              
124             sub insights {
125 1     1 1 105 my $self = shift;
126              
127 1         5 return $self->_response_for(
128             'insights',
129             'GeoIP2::Model::Insights',
130             @_,
131             );
132             }
133              
134             my %spec = (
135             ip => {
136             callbacks => {
137             'is a public IP address or me' => sub {
138             return defined $_[0]
139             && ( $_[0] eq 'me'
140             || is_public_ip( $_[0] ) );
141             }
142             },
143             },
144             );
145              
146             sub _response_for {
147 22     22   31 my $self = shift;
148 22         26 my $path = shift;
149 22         28 my $model_class = shift;
150              
151 22         300 my %p = validate( @_, \%spec );
152              
153 16         1641 my $uri = $self->_base_uri()->clone();
154 16         276 $uri->path_segments( $uri->path_segments(), $path, $p{ip} );
155              
156 16         1769 my $request = HTTP::Request->new(
157             'GET', $uri,
158             HTTP::Headers->new( Accept => 'application/json' ),
159             );
160              
161 16         2760 $request->authorization_basic(
162             $self->account_id(),
163             $self->license_key(),
164             );
165              
166 16         1122 my $response = $self->ua()->request($request);
167              
168 16 100       2444 if ( $response->code() == 200 ) {
169 7         88 my $body = $self->_handle_success( $response, $uri );
170             return $model_class->new(
171 6         18 %{$body},
  6         138  
172             locales => $self->locales(),
173             );
174             }
175             else {
176             # all other error codes throw an exception
177 9         104 $self->_handle_error_status( $response, $uri, $p{ip} );
178             }
179             }
180              
181             sub _handle_success {
182 7     7   13 my $self = shift;
183 7         11 my $response = shift;
184 7         12 my $uri = shift;
185              
186 7         12 my $body;
187             try {
188 7     7   336 $body = $self->_json()->decode( $response->decoded_content() );
189             }
190             catch {
191 1     1   132 GeoIP2::Error::Generic->throw(
192             message =>
193             "Received a 200 response for $uri but could not decode the response as JSON: $_",
194             );
195 7         53 };
196              
197 6         15864 return $body;
198             }
199              
200             sub _handle_error_status {
201 9     9   16 my $self = shift;
202 9         11 my $response = shift;
203 9         13 my $uri = shift;
204 9         15 my $ip = shift;
205              
206 9         15 my $status = $response->code();
207              
208 9 100       150 if ( $status =~ /^4/ ) {
    100          
209 7         17 $self->_handle_4xx_status( $response, $status, $uri, $ip );
210             }
211             elsif ( $status =~ /^5/ ) {
212 1         4 $self->_handle_5xx_status( $status, $uri );
213             }
214             else {
215 1         6 $self->_handle_non_200_status( $status, $uri );
216             }
217             }
218              
219             sub _handle_4xx_status {
220 7     7   13 my $self = shift;
221 7         9 my $response = shift;
222 7         10 my $status = shift;
223 7         10 my $uri = shift;
224 7         8 my $ip = shift;
225              
226 7 100       17 if ( $status == 404 ) {
227 2         19 GeoIP2::Error::IPAddressNotFound->throw(
228             message => "No record found for IP address $ip",
229             ip_address => $ip,
230             );
231             }
232              
233 5         14 my $content = $response->decoded_content();
234              
235 5         2105 my $body = {};
236              
237 5 100 66     25 if ( defined $content && length $content ) {
238 4 100       19 if ( $response->content_type() =~ /json/ ) {
239             try {
240 3     3   145 $body = $self->_json()->decode($content);
241             }
242             catch {
243 1     1   18 GeoIP2::Error::HTTP->throw(
244             message =>
245             "Received a $status error for $uri but it did not include the expected JSON body: $_",
246             http_status => $status,
247             uri => $uri,
248             );
249 3         107 };
250             GeoIP2::Error::Generic->throw( message =>
251             'Response contains JSON but it does not specify code or error keys'
252 2 100 66     40 ) unless $body->{code} && $body->{error};
253             }
254             else {
255 1         26 GeoIP2::Error::HTTP->throw(
256             message =>
257             "Received a $status error for $uri with the following body: $content",
258             http_status => $status,
259             uri => $uri,
260             );
261             }
262             }
263             else {
264 1         6 GeoIP2::Error::HTTP->throw(
265             message => "Received a $status error for $uri with no body",
266             http_status => $status,
267             uri => $uri,
268             );
269             }
270              
271             GeoIP2::Error::WebService->throw(
272             message => delete $body->{error},
273 1         4 %{$body},
  1         12  
274             http_status => $status,
275             uri => $uri,
276             );
277             }
278              
279             sub _handle_5xx_status {
280 1     1   4 my $self = shift;
281 1         3 my $status = shift;
282 1         2 my $uri = shift;
283              
284 1         5 GeoIP2::Error::HTTP->throw(
285             message => "Received a server error ($status) for $uri",
286             http_status => $status,
287             uri => $uri,
288             );
289             }
290              
291             sub _handle_non_200_status {
292 1     1   3 my $self = shift;
293 1         2 my $status = shift;
294 1         2 my $uri = shift;
295              
296 1         6 GeoIP2::Error::HTTP->throw(
297             message =>
298             "Received a very surprising HTTP status ($status) for $uri",
299             http_status => $status,
300             uri => $uri,
301             );
302             }
303              
304             sub _build_base_uri {
305 14     14   116 my $self = shift;
306              
307 14         76 return URI->new( 'https://' . $self->host() . '/geoip/v2.1' );
308             }
309              
310             sub _build_ua {
311 2     2   201 my $self = shift;
312              
313 2         11 return LWP::UserAgent->new();
314             }
315              
316             1;
317              
318             # ABSTRACT: Perl API for the GeoIP2 Precision web services
319              
320             __END__
321              
322             =pod
323              
324             =encoding UTF-8
325              
326             =head1 NAME
327              
328             GeoIP2::WebService::Client - Perl API for the GeoIP2 Precision web services
329              
330             =head1 VERSION
331              
332             version 2.006002
333              
334             =head1 SYNOPSIS
335              
336             use 5.008;
337              
338             use GeoIP2::WebService::Client;
339              
340             # This creates a Client object that can be reused across requests.
341             # Replace "42" with your account id and "abcdef123456" with your license
342             # key.
343             my $client = GeoIP2::WebService::Client->new(
344             account_id => 42,
345             license_key => 'abcdef123456',
346             );
347              
348             # Replace "insights" with the method corresponding to the web service
349             # that you are using, e.g., "country", "city".
350             my $insights = $client->insights( ip => '24.24.24.24' );
351              
352             my $country = $insights->country();
353             print $country->iso_code(), "\n";
354              
355             =head1 DESCRIPTION
356              
357             This class provides a client API for all the GeoIP2 Precision web service end
358             points. The end points are Country, City, and Insights. Each end point returns
359             a different set of data about an IP address, with Country returning the least
360             data and Insights the most.
361              
362             Each web service end point is represented by a different model class, and
363             these model classes in turn contain multiple Record classes. The record
364             classes have attributes which contain data about the IP address.
365              
366             If the web service does not return a particular piece of data for an IP
367             address, the associated attribute is not populated.
368              
369             The web service may not return any information for an entire record, in which
370             case all of the attributes for that record class will be empty.
371              
372             =head1 SSL
373              
374             Requests to the GeoIP2 web service are always made with SSL.
375              
376             =head1 USAGE
377              
378             The basic API for this class is the same for all of the web service end
379             points. First you create a web service object with your MaxMind C<account_id> and
380             C<license_key>, then you call the method corresponding to a specific end
381             point, passing it the IP address you want to look up.
382              
383             If the request succeeds, the method call will return a model class for the end
384             point you called. This model in turn contains multiple record classes, each of
385             which represents part of the data returned by the web service.
386              
387             If the request fails, the client class throws an exception.
388              
389             =head1 IP GEOLOCATION USAGE
390              
391             IP geolocation is inherently imprecise. Locations are often near the center of
392             the population. Any location provided by a GeoIP2 web service should not be
393             used to identify a particular address or household.
394              
395             =head1 CONSTRUCTOR
396              
397             This class has a single constructor method:
398              
399             =head2 GeoIP2::WebService::Client->new()
400              
401             This method creates a new client object. It accepts the following arguments:
402              
403             =over 4
404              
405             =item * account_id
406              
407             Your MaxMind Account ID. Go to L<https://www.maxmind.com/en/my_license_key> to see
408             your MaxMind Account ID and license key.
409              
410             B<Note>: This replaces a previous C<user_id> parameter, which is still
411             supported for backwards-compatibility, but should no longer be used for new
412             code.
413              
414             This argument is required.
415              
416             =item * license_key
417              
418             Your MaxMind license key. Go to L<https://www.maxmind.com/en/my_license_key> to
419             see your MaxMind Account ID and license key.
420              
421             This argument is required.
422              
423             =item * locales
424              
425             This is an array reference where each value is a string indicating a locale.
426             This argument will be passed onto record classes to use when their C<name()>
427             methods are called.
428              
429             The order of the locales is significant. When a record class has multiple
430             names (country, city, etc.), its C<name()> method will look at each element of
431             this array ref and return the first locale for which it has a name.
432              
433             Note that the only locale which is always present in the GeoIP2 data in "en".
434             If you do not include this locale, the C<name()> method may end up returning
435             C<undef> even when the record in question has an English name.
436              
437             Currently, the valid list of locale codes is:
438              
439             =over 8
440              
441             =item * de - German
442              
443             =item * en - English
444              
445             English names may still include accented characters if that is the accepted
446             spelling in English. In other words, English does not mean ASCII.
447              
448             =item * es - Spanish
449              
450             =item * fr - French
451              
452             =item * ja - Japanese
453              
454             =item * pt-BR - Brazilian Portuguese
455              
456             =item * ru - Russian
457              
458             =item * zh-CN - simplified Chinese
459              
460             =back
461              
462             Passing any other locale code will result in an error.
463              
464             The default value for this argument is C<['en']>.
465              
466             =item * host
467              
468             The hostname to make a request against. This defaults to
469             "geoip.maxmind.com". In most cases, you should not need to set this
470             explicitly.
471              
472             =item * ua
473              
474             This argument allows you to your own L<LWP::UserAgent> object. This is useful
475             if you cannot use a vanilla LWP object, for example if you need to set proxy
476             parameters.
477              
478             This can actually be any object which supports C<agent()> and C<request()>
479             methods. This method will be called with an L<HTTP::Request> object as its
480             only argument. This method must return an L<HTTP::Response> object.
481              
482             =back
483              
484             =head1 REQUEST METHODS
485              
486             All of the request methods accept a single argument:
487              
488             =over 4
489              
490             =item * ip
491              
492             This must be a valid IPv4 or IPv6 address, or the string "me". This is the
493             address that you want to look up using the GeoIP2 web service.
494              
495             If you pass the string "me" then the web service returns data on the client
496             system's IP address. Note that this is the IP address that the web service
497             sees. If you are using a proxy, the web service will not see the client
498             system's actual IP address.
499              
500             =back
501              
502             =head2 $client->country()
503              
504             This method calls the GeoIP2 Precision: Country end point. It returns a
505             L<GeoIP2::Model::Country> object.
506              
507             =head2 $client->city()
508              
509             This method calls the GeoIP2 Precision: City end point. It returns a
510             L<GeoIP2::Model::City> object.
511              
512             =head2 $client->insights()
513              
514             This method calls the GeoIP2 Precision: Insights end point. It returns a
515             L<GeoIP2::Model::Insights> object.
516              
517             =head1 User-Agent HEADER
518              
519             This module will set the User-Agent header to include the package name and
520             version of this module (or a subclass if you use one), the package name and
521             version of the user agent object, and the version of Perl.
522              
523             This is set in order to help us support individual users, as well to determine
524             support policies for dependencies and Perl itself.
525              
526             =head1 EXCEPTIONS
527              
528             For details on the possible errors returned by the web service itself, see
529             L<http://dev.maxmind.com/geoip/geoip2/web-services> for the GeoIP2 web service
530             docs.
531              
532             If the web service returns an explicit error document, this is thrown as a
533             L<GeoIP2::Error::WebService> exception object. If some other sort of error
534             occurs, this is thrown as a L<GeoIP2::Error::HTTP> object. The difference is
535             that the web service error includes an error message and error code delivered
536             by the web service. The latter is thrown when some sort of unanticipated error
537             occurs, such as the web service returning a 500 or an invalid error document.
538              
539             If the web service returns any status code besides 200, 4xx, or 5xx, this also
540             becomes a L<GeoIP2::Error::HTTP> object.
541              
542             Finally, if the web service returns a 200 but the body is invalid, the client
543             throws a L<GeoIP2::Error::Generic> object.
544              
545             All of these error classes have an C<< $error->message() >> method and
546             overload stringification to show that message. This means that if you don't
547             explicitly catch errors they will ultimately be sent to C<STDERR> with some
548             sort of (hopefully) useful error message.
549              
550             =head1 WHAT DATA IS RETURNED?
551              
552             While many of the end points return the same basic records, the attributes
553             which can be populated vary between end points. In addition, while an end
554             point may offer a particular piece of data, MaxMind does not always have every
555             piece of data for any given IP address.
556              
557             Because of these factors, it is possible for any end point to return a record
558             where some or all of the attributes are unpopulated.
559              
560             See L<http://dev.maxmind.com/geoip/geoip2/web-services> for details on what data each end
561             point I<may> return.
562              
563             The only piece of data which is always returned is the C<ip_address> key in
564             the C<GeoIP2::Record::Traits> record.
565              
566             Every record class attribute has a corresponding predicate method so you can
567             check to see if the attribute is set.
568              
569             =head1 SUPPORT
570              
571             Bugs may be submitted through L<https://github.com/maxmind/GeoIP2-perl/issues>.
572              
573             =head1 AUTHORS
574              
575             =over 4
576              
577             =item *
578              
579             Dave Rolsky <drolsky@maxmind.com>
580              
581             =item *
582              
583             Greg Oschwald <goschwald@maxmind.com>
584              
585             =item *
586              
587             Mark Fowler <mfowler@maxmind.com>
588              
589             =item *
590              
591             Olaf Alders <oalders@maxmind.com>
592              
593             =back
594              
595             =head1 COPYRIGHT AND LICENSE
596              
597             This software is copyright (c) 2013 - 2019 by MaxMind, Inc.
598              
599             This is free software; you can redistribute it and/or modify it under
600             the same terms as the Perl 5 programming language system itself.
601              
602             =cut