File Coverage

blib/lib/NOLookup/RDAP/RDAPLookup.pm
Criterion Covered Total %
statement 113 398 28.3
branch 30 206 14.5
condition 2 27 7.4
subroutine 23 31 74.1
pod 8 8 100.0
total 176 670 26.2


line stmt bran cond sub pod time code
1             package NOLookup::RDAP::RDAPLookup;
2              
3 1     1   131007 use warnings;
  1         11  
  1         33  
4 1     1   5 use strict;
  1         2  
  1         22  
5 1     1   4 use POSIX qw(locale_h);
  1         2  
  1         7  
6 1     1   2198 use URI::Encode qw(uri_encode);
  1         1409  
  1         58  
7 1     1   554 use URI;
  1         6855  
  1         30  
8 1     1   2280 use JSON;
  1         13943  
  1         6  
9 1     1   117 use base qw(Class::Accessor::Chained); ## Provides a new() method
  1         2  
  1         459  
10 1     1   4494 use Net::DNS::Domain;
  1         5784  
  1         35  
11 1     1   2212 use Net::IP;
  1         65089  
  1         148  
12 1     1   1959 use List::MoreUtils qw(any);
  1         17403  
  1         7  
13 1     1   1810 use Text::Wrap;
  1         4856  
  1         84  
14 1     1   8 use base qw(Net::RDAP);
  1         2  
  1         842  
15 1     1   948011 use Net::RDAP::EPPStatusMap;
  1         409  
  1         62  
16 1     1   9 use Net::RDAP::Error;
  1         3  
  1         20  
17 1     1   445 use NOLookup;
  1         4  
  1         30  
18              
19             # debug only for dev env.
20             #use LWP::ConsoleLogger::Easy qw /debug_ua /;
21 1     1   511 use Sys::HostIP;
  1         1883  
  1         52  
22 1     1   1199 use Data::Validate::IP qw(is_ip);
  1         54863  
  1         72  
23              
24 1     1   617 use Data::Dumper;
  1         6507  
  1         79  
25             $Data::Dumper::Indent=1;
26              
27             our $VERSION = $NOLookup::VERSION;
28              
29 1     1   20 use vars qw(@ISA @EXPORT_OK);
  1         2  
  1         4775  
30              
31             @ISA = qw( Exporter Class::Accessor::Chained Net::RDAP);
32             @EXPORT_OK = qw / $RDAP_LOOKUP_ERR_NO_CONN
33              
34             $RDAP_LOOKUP_ERR_QUOTA_EXCEEDED
35             $RDAP_LOOKUP_ERR_NO_ACCESS
36             $RDAP_LOOKUP_ERR_REFERRAL_DENIED
37             $RDAP_LOOKUP_ERR_OTHER
38             $RDAP_LOOKUP_ERR_NO_MATCH
39             $RDAP_LOOKUP_ERR_NO_CONFORMANCE
40             $RDAP_LOOKUP_ERR_INVALID
41             /;
42              
43             # Connection problems
44             our $RDAP_LOOKUP_ERR_NO_CONN = 100;
45              
46             # Controlled refuses
47             our $RDAP_LOOKUP_ERR_QUOTA_EXCEEDED = 101;
48             our $RDAP_LOOKUP_ERR_NO_ACCESS = 102;
49             our $RDAP_LOOKUP_ERR_REFERRAL_DENIED = 103;
50             our $RDAP_LOOKUP_ERR_OTHER = 104;
51             our $RDAP_LOOKUP_ERR_NO_MATCH = 105;
52             our $RDAP_LOOKUP_ERR_NO_CONFORMANCE = 106;
53             our $RDAP_LOOKUP_ERR_INVALID = 107;
54              
55             my $RDAP_TIMEOUT = 60; # secs (default is 180 secs but we want shorter time).
56              
57             # Default API service URL
58             my $SERVICE_URL = "https://rdap.norid.no";
59              
60             ############
61             #
62             # RDAP API. See section 'About the Norid RDAP API' below for more info.
63             #
64              
65             my %rdap_head_get_method_args = (
66             DBDN => 'domain/',
67             EBEH => 'entity/',
68             NBNH => 'nameserver_handle/'
69             );
70              
71             # Not used RBRH => 'registrar/', same as EBEH => 'entity/'
72              
73             my %rdap_get_method_args = (
74             NBNN => 'nameservers?name=',
75             DBID => 'domains?identity=',
76             DBRH => 'domains?registrant=',
77             EBID => 'entities?identity='
78             );
79              
80             my $ht;
81              
82             ##
83             # RDAP API conformance values.
84             # This library supports and expects the following conformance values.
85             # if other values are returned, the library _may_ need an upgrade, so the
86             # lookup will simply fail if any conformance mismatch is detected.
87             #
88              
89             my %rdapConformance_vals_supported = (
90             'rdap_level_0' => 1,
91             'rdap_objectTag_level_0' => 1,
92             'norid_level_0' => 1
93             );
94              
95             # Some accessor methods.
96             # Those starting with '_' are meant for internal use.
97             my @methods = qw /
98              
99             warning
100             error
101             status
102              
103             result
104             raw_json_decoded
105              
106             _method
107             _uri
108             _full_url
109              
110             /;
111              
112             __PACKAGE__->mk_accessors(
113             @methods
114             );
115              
116             # Set an env var to suppress warnings from Net::RDAP
117             unless ($ENV{'NET_RDAP_UA_DEBUG'}) {
118             $ENV{'NET_RDAP_UA_DEBUG'} = 0;
119             }
120              
121             =head2 new
122              
123             new handles the following parameters:
124              
125             {
126             debug => <0|1|5>,
127             use_cache => <0|1>,
128             service_url => <0|service_url,
129             norid_header_secret => <0|test_secret>,
130             norid_header_proxy => <0|1>,
131             norid_referral_ip => <0|1|ip-address>,
132             }
133              
134             All parameters are optional:
135              
136             * use_cache:
137             - 1: activate lookup cache, see Net::RDAP for use
138              
139             * debug:
140             - 0: debug off
141             - 1: debug from this module on
142             - 5: full debug from Net::RDAP on, see Net::RDAP for use
143              
144             * service_url:
145             - the full http(s)-address of the Norid RDAP-servie to
146             be accessed.
147             - default is $SERVICE_URL above.
148              
149             * norid_header_secret:
150             - access token for layered access, and
151             the token is sent in the 'X-RDAP-Secret' header.
152              
153             * norid_header_proxy :
154             - Norid internal use only.
155             - true if the calling client can act as a proxy,
156             and the header 'X-RDAP-Web-Proxy' is then set to 1.
157              
158             * norid_referral_ip :
159             - Norid internal use only.
160             - set if the calling client ip address argument shall be sent. When set:
161             - if the argument passed is a pure integer, use the local ip-address as value.
162             - if the argument passed is a valid ip address, use that address
163             as value. This is the normal variant to be used to pass a proper client
164             ip address.
165             - the ip-address is passed to the server in the '?client_ip=<ip-address>'
166             argument.
167              
168             =cut
169              
170             sub new {
171 4     4 1 8740 my ($self, $args)=@_;
172              
173             # defaults
174 4 50       16 $args->{service_url} = $SERVICE_URL unless ($args->{service_url});
175              
176 4 50 33     15 if ($args->{norid_referral_ip} && $args->{norid_referral_ip} =~ m/^\d+$/) {
177             # Set to true (a pure number) - then select a local ip-address
178 0         0 delete($args->{norid_referral_ip});
179 0         0 my $ip = get_my_ip_address();
180 0 0       0 $args->{norid_referral_ip} = $ip if ($ip);
181             }
182              
183 4         24 $self->SUPER::new($args);
184              
185             }
186              
187             =head2 lookup
188              
189             Do an RDAP lookup.
190              
191             - $query : specifies the query string
192             - $check : specifies if http 'head' shall be done, default is 'get'.
193             - $nameservers: must be set to true for nameserver_name search
194             - $entity : must be set to true for entity lookup, in which case the query should
195             identify an entity, like:
196             - a domain name
197             - a handle, like registrar handle, registrant handle, ns handle or contact handle
198             must be set to false to trig a search if the query
199             contains something that allows a search, like:
200             - identity (organization number, N.PRI.xxx etc)
201             - a registrant handle (P- or O- handle)
202             - a nameserver name (must then be combined with
203             $nameservers=1 to distinguish from a domain name)
204              
205             =cut
206              
207             sub lookup {
208 4     4 1 1901 my ($self, $query, $check, $nameservers, $entity ) = @_;
209              
210             #print STDERR "RDAPLookup: lookup on query: $query, check: ", $check || 0, ", nameservers: ", $nameservers || 0, ", entity: ", $entity || 0, "\n";
211              
212 4 50       23 unless ($self->_validate_and_analyze($query, $check, $nameservers, $entity)) {
213             # errno has already been set
214 0         0 return $self;
215             }
216             # _method (head or get) and args are set in $self
217 4         13 $self->_lookup_rdap($query, $self->_method, $self->_uri);
218            
219             }
220              
221             =head2 _lookup_rdap
222              
223             Do an RDAP HEAD or GET lookup.
224              
225             - $http_type: 'head' or 'get'
226             - $uri : full arguments to base URI, identifying the actual lookup
227             method and args
228             - other args as passed in $self.
229              
230             =cut
231              
232             sub _lookup_rdap {
233 4     4   113 my ($self, $query, $http_type, $uri ) = @_;
234              
235 4         30 my $ua = $self->ua;
236              
237             # debug only for dev env.
238             # if ($self->{debug} && $self->{debug} > 1) {
239             # debug_ua ( $ua, 5 );
240             # }
241              
242 4         5009 $ua->default_header( Charset => "UTF-8");
243 4         293 $ua->default_header( 'Content-Type' => "application/rdap+json");
244              
245 4 50       211 if ($self->{norid_header_secret}) {
246             # Use Norid RDAP layer secret headers
247 0         0 $ua->default_header( 'X-RDAP-Secret' => $self->{norid_header_secret});
248             }
249              
250 4 50       21 if ($self->{norid_header_proxy}) {
251             # Use Norid RDAP proxy headers
252 0         0 $ua->default_header( 'X-RDAP-Web-Proxy' => 1);
253             }
254              
255 4         22 my $URL = $self->{service_url} . "/$uri";
256              
257 4 50       27 if ($self->{norid_referral_ip}) {
258 0         0 my $a = '?';
259 0 0       0 if ($URL =~ m/\?/) {
260             # args already exists, use '&' for extra arg
261 0         0 $a = '&';
262             }
263 0         0 $URL .= $a ."client_ip=" . $self->{norid_referral_ip};
264             }
265            
266 4         23 $self->_full_url($URL);
267              
268 4 50       76 if ($self->{debug}) {
269 0         0 print STDERR "_lookup-rdap called with URL: '$URL', and:\n";
270 0 0       0 print STDERR " proxy is : ", $self->{norid_header_proxy} , "\n" if ($self->{norid_header_proxy});
271 0 0       0 print STDERR " secret is : ", $self->{norid_header_secret}, "\n" if ($self->{norid_header_secret});
272 0 0       0 print STDERR " referral_ip: ", $self->{norid_referral_ip} , "\n" if ($self->{norid_referral_ip});
273             }
274              
275 4         9 my $resp;
276            
277 4 100       20 if ($http_type eq 'head') {
278             # An RDAP HEAD operation. Head is not supported by Net::RDAP,
279             # so call it via the already created UA
280 2         21 $resp = $ua->head($URL);
281 2 50       524731 unless ($resp->is_success) {
282 0         0 $self->error(_map_rdap_error($query, $resp->code));
283 0         0 $self->status($resp->status_line);
284             }
285 2         87 return $self;
286             }
287              
288             # An RDAP GET operation.
289 2         15 $resp = $self->fetch(URI->new($URL));
290              
291 2 50       362343 unless ($resp) {
292             #print STDERR "Empty GET resp\n";
293 0         0 $self->error( _map_rdap_error($query, 404) );
294 0         0 $self->status("Lookup returned nothing!");
295 0         0 return $self;
296             }
297 2 50       31 if ($resp->isa('NOLookup::RDAP::RDAPLookup')) {
298             # a $resp is returned, but when fetch() finds nothing, it
299             # just returs the $self-object, possible with an error[] filled in,
300             # so handle it as nothing found
301              
302             #print STDERR "Nothing found returned 'NOLookup::RDAP::RDAPLookup' self resp\n";
303            
304 0         0 my $err = $resp->error;
305 0 0       0 if (ref($err) eq 'ARRAY') {
306 0         0 my $ix = 0;
307 0         0 foreach my $el (@$err) {
308 0 0       0 if ($el eq 'errorCode') {
    0          
309 0         0 $self->error( _map_rdap_error($query, $err->[$ix+1]) );
310             #print STDERR " ARRAY error:", $self->error, "\n";
311              
312             } elsif ($el eq 'title') {
313 0         0 $self->status($err->[$ix+1]);
314             }
315 0         0 ++$ix;
316             }
317             } else {
318 0         0 $self->error(_map_rdap_error($query, $err));
319             #print STDERR " scalar error:", $self->error, "\n";
320 0         0 $self->status("Lookup rejected or returned no match!");
321             }
322             #print STDERR " RDAPLookup.pm: lookup_error on URL: $URL, error: ", $self->error, "\n";
323 0         0 return $self;
324             }
325              
326 2 50       26 if ($resp->isa('Net::RDAP::Error')) {
327             #print STDERR "Error GET resp\n";
328 0         0 $self->error(_map_rdap_error($query,$resp->errorCode));
329 0         0 $self->status($resp->title);
330 0         0 return $self;
331             }
332              
333             # Check conformance values before we accept the answer
334 2         5 my @cf = @{$resp->{rdapConformance}};
  2         10  
335 2         7 foreach my $cfe (@cf) {
336 6 50       21 unless ($rdapConformance_vals_supported{$cfe}) {
337 0         0 $self->status("Conformance mismatch on key $cfe, this library does not support this RDAP version!");
338 0         0 $self->error($RDAP_LOOKUP_ERR_NO_CONFORMANCE);
339 0         0 return $self;
340             }
341             }
342              
343             # resp contains the json data
344 2         6 $self->raw_json_decoded(to_json({%{$resp}}, {utf8 => 1, pretty => 1}));
  2         71  
345              
346 2         315 $self->result($resp);
347            
348 2         42 return $self;
349             }
350              
351             =head2 _map_rdap_error
352              
353             Some RDAP error is returned from Net::RDAP, ref. Net::RDAP::Error.
354              
355             Those are normally HTTP response errors in the 400 and 500 range,
356             which are mapped to one of the $RDAP_LOOKUP_ERR_XXX local errors.
357              
358             https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
359             - All 1xx are not errors, but Ok, need not be handled.
360             - All 2xx are not errors, but Ok, need not be handled.
361             - All 3xx are redirection errors, which are not expected,
362             map to other if we get it.
363             - All 3xx are redirection errors, which are not expected,
364             map to other if we get it.
365            
366             All 5xx errors are considered connection problems at some level
367              
368             =cut
369              
370             sub _map_rdap_error {
371 0     0   0 my ($query, $rdap_error) = @_;
372              
373 0         0 my $rcode;
374 0 0       0 if ($rdap_error =~ m/^4\d+$/) {
    0          
375             # Some client side problem
376 0 0       0 if ($rdap_error == 404) {
    0          
377 0         0 $rcode = $RDAP_LOOKUP_ERR_NO_MATCH;
378             } elsif ($rdap_error == 429) {
379 0         0 $rcode = $RDAP_LOOKUP_ERR_QUOTA_EXCEEDED;
380             } else {
381 0         0 $rcode = $RDAP_LOOKUP_ERR_INVALID;
382             }
383              
384             } elsif ($rdap_error =~ m/^5\d+$/) {
385             # Some some server side problems
386 0 0       0 if ($rdap_error == 501) {
387 0         0 $rcode = $RDAP_LOOKUP_ERR_INVALID;
388             } else {
389 0         0 $rcode = $RDAP_LOOKUP_ERR_NO_CONN;
390             }
391              
392             } else {
393 0         0 $rcode = $RDAP_LOOKUP_ERR_OTHER;
394             # report other case in case the code can be mapped to a better value
395 0         0 print STDERR "_map_rdap_error - some other error code: '$rdap_error' was returned for query: $query\n";
396             }
397              
398 0         0 return $rcode;
399              
400             }
401              
402             =head2 get_my_ip_address
403              
404             Find local ip-address.
405              
406             (Note: Sys::HostAddr were also tried for this purpose, but could die at
407             random, so Sys::HostIP is selected. Only ipv4 seems to be processed by
408             Sys::HostIP, so the selection is limited to ipv4.
409             TODO: Consider using another module, which also supports v6).
410              
411             Best guess IP seems to be the one on the en0-interface, but when a VPN
412             is in use, you might want that address to be selected. So, try to do
413             the best ip selection by ourselves by a reverse sort instead of a
414             sort, thus selecting the 'highest' numbered and public ip-address).
415              
416             Return localhost if no other ip is found.
417              
418             Return empty if localhost iface not found.
419              
420             =cut
421              
422             sub get_my_ip_address {
423              
424 0     0 1 0 my $hostip = Sys::HostIP->new;
425            
426 0         0 my $if_info = $hostip->if_info;
427 0         0 my $lo_found;
428 0         0 foreach my $key ( reverse sort keys %{$if_info} ) {
  0         0  
429             # we don't want the loopback
430 0 0       0 if ( $if_info->{$key} eq '127.0.0.1' ) {
431 0         0 $lo_found++;
432 0         0 next;
433             }
434             # now we return the first one that comes up
435 0         0 return ( $if_info->{$key} );
436             }
437            
438             # we get here if loopback is the only active device
439 0 0       0 $lo_found and return '127.0.0.1';
440            
441 0         0 return '';
442              
443             }
444              
445             =head2 _validate_and_analyze
446              
447             1) Validate ip address, if set
448            
449             2) Validate query, return if query not among the expexted ones.
450             - domain name or name server name
451             - some object handle (D, P, R, H)
452             - some registrar handle (regXXX-NORID)
453             - some identity (9 digits orgno, N.XXX.yyyyyyyy)
454              
455             2) Analyze query and args and find what http method and uri arguments
456             to use for the lookup and set them in '_method' and '_uri'
457              
458             =cut
459              
460             sub _validate_and_analyze {
461 4     4   19 my ($self, $q, $check, $ns, $entity) = @_;
462              
463 4 50       29 if (my $ip = $self->{norid_referral_ip}) {
464 0 0       0 unless (is_ip($ip)) {
465 0         0 $self->status("Invalid referral ip address: $ip");
466 0         0 $self->error($RDAP_LOOKUP_ERR_INVALID);
467 0         0 return 0;
468             }
469             }
470            
471 4         8 my $arg;
472            
473 4 50       27 $q =~ s/\s+//g if ($q);
474              
475 4 50       19 unless ($q) {
476 0         0 $self->status("mandatory query parameter not specified");
477 0         0 $self->error($RDAP_LOOKUP_ERR_INVALID);
478 0         0 return 0;
479             }
480              
481 4 50       77 if ($q =~ m/^\d{9}$/) {
    50          
    50          
    50          
    50          
482             # org number OK
483 0 0       0 if ($q eq "000000000") {
    0          
484 0         0 $self->status("Invalid ID: $q");
485 0         0 $self->error($RDAP_LOOKUP_ERR_INVALID);
486 0         0 return 0;
487              
488             } elsif ($q !~ m/^[8|9]\d{8}/) {
489 0         0 $self->status("Invalid ID, org.number must start on 8 or 9: $q");
490 0         0 $self->error($RDAP_LOOKUP_ERR_INVALID);
491 0         0 return 0;
492             }
493             # Search domains by identity to get list of domain names for that orgno
494 0         0 $arg = 'DBID';
495 0 0       0 if ($entity) {
496             # search entities by identity to get list of handles with that orgno
497 0         0 $arg = 'EBID';
498             }
499              
500             } elsif ($q =~ /^N\.(PRI|LEG|ORG|REG)\.\d+$/i) {
501 0         0 $q = uc($q);
502              
503             # Some other identity
504             # domains by identity is default
505 0         0 $arg = 'DBID';
506 0 0       0 if ($entity) {
507             # entities by identity
508 0         0 $arg = 'EBID';
509             }
510              
511             } elsif ($q =~ /REG(\d+)-NORID$/i) {
512             # registrar handle lookup
513             # is case sensitive, syntax: 'reg2-NORID'
514 0         0 $q = "reg$1-NORID";
515              
516             # registrar by reg handle RBRH, same as EBEH, so use that
517 0         0 $arg = 'EBEH';
518            
519             } elsif ($q =~ /.+([PORH])-NORID$/i) {
520             # P, O, R or H handle
521             # Note D-handle lookup is not supported by the rdap, use the domain name instead
522 0         0 $q = uc($q);
523              
524 0         0 my $ot = uc($1);
525            
526 0 0 0     0 if ($ot eq 'P' || $ot eq 'O') {
    0          
527             # is a registrant handle
528              
529             # domains by registrant handle is default
530 0         0 $arg = 'DBRH';
531 0 0       0 if ($entity) {
532             # entities by entity handle
533 0         0 $arg = 'EBEH';
534             }
535              
536             } elsif ($ot eq 'H') {
537             # is a name server handle
538              
539             # nameserver_handle by nameserver handle is default
540 0         0 $arg = 'NBNH';
541 0 0       0 if ($entity) {
542             # entity by entity handle not possible here
543             #$arg = 'EBEH';
544             }
545              
546             } else {
547             # is a D or R, only option is lookup entity by entity handle
548 0         0 $arg = 'EBEH';
549             }
550            
551             } elsif ($q =~ /.+\..+$|^\.(no.*)$/i) {
552              
553             # Some string with a dot in it is assumed to be a domain name or name server
554             # name, or just 'no' itself
555 4         19 $q = lc($q);
556              
557             # TODO: if $1, we have no alone to be looked up, maybe RDAP
558             # will need only one syntax, like 'no.' for the name,
559             # adjust $q to comply to the rule if it comes.
560 4 50       21 if ($1) {
561             #print "STDERR: a single no domain lookup requested, q: $q\n";
562             # adjust $q here:
563             }
564            
565             # domain by domain name is default
566 4         11 $arg = 'DBDN';
567 4 50       15 if ($ns) {
568             # nameservers by name server name (NBNN)
569 0         0 $arg = 'NBNN';
570             }
571            
572             } else {
573 0         0 $self->status("Invalid query, not supported by the RDAP service: $q");
574 0         0 $self->error($RDAP_LOOKUP_ERR_INVALID);
575 0         0 return 0;
576             }
577              
578 4 50       18 unless ($arg) {
579 0         0 $self->status("No success in finding a lookup method, try a valid query combination: $q");
580 0         0 $self->error($RDAP_LOOKUP_ERR_INVALID);
581 0         0 return 0;
582             }
583              
584 4 50       15 unless ($arg) {
585 0         0 $self->status("No success in finding a lookup method, try a valid query combination: $q");
586 0         0 $self->error($RDAP_LOOKUP_ERR_INVALID);
587 0         0 return 0;
588             }
589              
590 4 100       15 if ($check) {
591 2 50       10 unless ($rdap_head_get_method_args{$arg}) {
592 0         0 $self->status("No success in finding a HEAD lookup method for $arg, try a valid query combination: $q");
593 0         0 $self->error($RDAP_LOOKUP_ERR_INVALID);
594 0         0 return 0;
595             }
596 2         13 $self->_method('head');
597 2         57 $self->_uri($rdap_head_get_method_args{$arg} . $q);
598              
599             } else {
600 2 0 33     11 unless ($rdap_head_get_method_args{$arg} || $rdap_get_method_args{$arg}) {
601 0         0 $self->status("No success in finding a GET lookup method for $arg, try a valid query combination: $q");
602 0         0 $self->error($RDAP_LOOKUP_ERR_INVALID);
603 0         0 return 0;
604             }
605              
606 2         13 $self->_method('get');
607              
608 2 50       49 if ($rdap_head_get_method_args{$arg}) {
609 2         13 $self->_uri($rdap_head_get_method_args{$arg} . $q);
610             } else {
611 0         0 $self->_uri($rdap_get_method_args{$arg} . $q);
612             }
613             }
614             # remember the query
615 4         64 $self->{query} = $q;
616              
617 4         19 return 1;
618             }
619              
620             =head2 result_as_rdap_string
621              
622             Return sensible rdap formatted string.
623             Uses internal helper formatting functions.
624              
625             Shows how to access data returned by Net::RDAP.
626              
627             =cut
628              
629             sub result_as_rdap_string {
630 0     0 1   my ($self, $check, $nameservers, $entity, $short, $expand) = @_;
631              
632 0           my @errors;
633              
634 0           my $response = $self->result;
635              
636 0           my $rs = "";
637            
638 0 0         if ($response->isa('Net::RDAP::Error')) {
    0          
639 0           push(@errors, sprintf("%03u (%s)", $response->errorCode, $response->title));
640            
641             } elsif ($response->isa('Net::RDAP::SearchResult')) {
642              
643 0           foreach my $o ($response->nameservers, $response->domains, $response->entities) {
644 0           my ($rst, $errs) = $self->rdap_obj_as_string($o, $check, $nameservers, $entity, $short, $expand);
645 0 0         $rs .= $rst if ($rst);
646 0 0 0       push @errors, @$errs if ($errs && @$errs);
647              
648             }
649              
650             } else {
651              
652 0           my ($rst, $errs) = $self->rdap_obj_as_string($response, $check, $nameservers, $entity, $short, $expand);
653 0 0         $rs .= $rst if ($rst);
654 0 0 0       push @errors, @$errs if ($errs && @$errs);
655             }
656              
657 0           return $rs, \@errors;
658              
659             }
660              
661             =head2 rdap_obj_as_string
662              
663             Return sensible rdap formatted string.
664              
665             Code stolen from rdapper and adapted.
666              
667             =cut
668              
669             sub rdap_obj_as_string {
670 0     0 1   my ($self, $response, $check, $nameservers, $entity, $short, $expand) = @_;
671              
672 0           my @errors;
673 0           my $rs = "";
674            
675 0 0         if ('entity' ne $response->class) {
676              
677 0           my ($name, $xname) = $self->rdap_get_obj_name($response);
678              
679 0 0         if ($xname ne $name) {
680 0           $rs .= sprintf("\nName: %s (%s)\n\n", $xname, $name);
681             } else {
682 0           $rs .= sprintf("\nName: %s\n\n", $name);
683             }
684             }
685              
686 0           $rs .= sprintf("Handle: %s\n\n", $response->handle);
687              
688 0 0         if ('ip network' eq $response->class) {
    0          
    0          
    0          
    0          
689 0           $rs .= sprintf("Range: %s\n\n", $response->range->prefix);
690 0           $rs .= sprintf("Domain: %s\n\n", $response->domain->as_string);
691            
692             } elsif ('autnum' eq $response->class) {
693 0 0 0       $rs .= sprintf("Range: %u - %u\n\n", $response->start, $response->end) if ($response->start > 0 && $response->end > 0);
694 0 0         $rs .= sprintf("Type: %s\n\n", $response->type) if ($response->type);
695            
696             } elsif ('domain' eq $response->class) {
697 0           my @ns = $response->nameservers;
698 0 0         if (scalar(@ns) > 0) {
699 0           $rs .= "Nameservers:\n\n";
700 0           foreach my $ns (sort { lc($a->name->name) cmp lc($b->name->name) } @ns) {
  0            
701 0           $rs .= sprintf(" %s\n", $ns->name->name);
702             }
703 0           $rs .= "\n";
704             }
705            
706 0           my @ds = $response->ds;
707 0 0         if (scalar(@ds) > 0) {
708 0           $rs .= "DNSSEC:\n\n";
709 0           foreach my $ds ($response->ds) {
710 0           $rs .= sprintf(" %s. IN DS %u %u %u %s\n", uc($ds->name),
711             $ds->keytag, $ds->algorithm, $ds->digtype, uc($ds->digest));
712             }
713 0           $rs .= "\n";
714             }
715            
716 0           my @keys = $response->keys;
717 0 0         if (scalar(@keys) > 0) {
718 0           $rs .= "DNSSEC Keys:\n\n";
719 0           foreach my $key (@keys) {
720 0           $rs .= sprintf(" %s. IN DNSKEY %u %u %u %s\n", uc($key->name), $key->flags, $key->protocol, $key->algorithm, uc($key->key));
721             }
722 0           $rs .= "\n";
723             }
724            
725             } elsif ('entity' eq $response->class) {
726 0           $rs .= $self->rdap_vcard_as_string($response->vcard, ' ' x 2);
727            
728             } elsif ('nameserver' eq $response->class) {
729 0           $rs .= "IP Addresses:\n\n";
730              
731 0           my @addrs = $response->addresses;
732 0 0         if (scalar(@addrs) > 0) {
733 0           foreach my $ip (@addrs) {
734 0           $rs .= sprintf(" * %s\n", $ip->ip);
735             }
736             } else {
737 0           $rs .= " * (no IP addresses returned)\n";
738             }
739 0           $rs .= "\n";
740             }
741              
742 0           my @events = $response->events;
743 0 0         if (scalar(@events)) {
744 0           $rs .= "Events:\n\n";
745 0           foreach my $event (@events) {
746             # DateTime object is UTC, convert to localtime
747 0           my $to = $event->date;
748 0           $to->set_time_zone('Europe/Oslo');
749 0           $rs .= sprintf(" %s: %s\n", ucfirst($event->action), scalar($to->date));
750             }
751 0           $rs .= "\n";
752             }
753            
754 0           my @status = $response->status;
755 0 0         if (scalar(@status) > 0) {
756 0           $rs .= "Status:\n\n";
757 0           foreach my $status (@status) {
758 0           my $epp = rdap2epp($status);
759 0 0         if ($epp) {
760 0           $rs .= sprintf(" * %s (EPP: %s)\n", $status, $epp);
761            
762             } else {
763 0           $rs .= sprintf(" * %s\n", $status);
764             }
765             }
766 0           $rs .= "\n";
767             }
768            
769 0           my @entities = $response->entities;
770 0           my %entities;
771 0           foreach my $ent (@entities) {
772            
773 0 0 0       if (!$ent->vcard && $expand) {
774              
775             my $ro = NOLookup::RDAP::RDAPLookup->new(
776             {
777             service_url => $self->{service_url},
778             debug => $self->{debug},
779             use_cache => $self->{use_cache},
780             norid_header_secret => $self->{norid_header_secret},
781             norid_header_proxy => $self->{norid_header_proxy},
782 0           });
783            
784 0           my $new = $ro->lookup($ent->handle, $check, $nameservers, 1);
785            
786 0 0         if ($new->isa('Net::RDAP::Error')) {
787 0           push(@errors, sprintf('Unable to expand %s: %d (%s)',
788             $ent->handle, $new->errorCode, $new->title));
789             } else {
790 0           $ent = $new->result;
791             }
792             }
793            
794 0           map { $entities{$_} = $ent } $ent->roles;
  0            
795             }
796            
797 0 0         if (scalar(@entities) > 0) {
798 0           $rs .= "Entities:\n\n";
799            
800 0           foreach my $entity (@entities) {
801            
802 0           my @roles = $entity->roles;
803 0 0         if (scalar(@roles) > 0) {
804 0 0         if ($entity->handle) {
805 0           $rs .= sprintf(" Entity %s (%s):\n\n", $entity->handle, join(', ', sort(@roles)));
806            
807             } else {
808 0           $rs .= sprintf(" %s:\n\n", join(', ', map { sprintf('%s Contact', ucfirst($_)) } sort(@roles)));
  0            
809            
810             }
811            
812             } else {
813 0           $rs .= sprintf(" Entity %s:\n\n", $entity->handle);
814            
815             }
816            
817 0           my $card = $entity->vcard;
818 0 0         if (!$card) {
819 0           $rs .= " (no further information available)\n\n";
820            
821             } else {
822 0           $rs .= $self->rdap_vcard_as_string($card, ' ' x 4);
823            
824             }
825             }
826             }
827            
828 0 0         if (!$short) {
829 0           my @links = $response->links;
830 0 0         if (scalar(@links) > 0) {
831 0           $rs .= "Links:\n";
832 0           foreach my $link (@links) {
833 0   0       $rs .= sprintf("\n * %s (%s)\n", $link->href->as_string, $link->title || $link->rel || '-');
834             }
835 0           $rs .= "\n";
836             }
837            
838 0           my @remarks = $response->remarks;
839 0 0         if (scalar(@remarks) > 0) {
840 0           $rs .= "Remarks:\n\n";
841 0           foreach my $remark (@remarks) {
842 0           my $indent = ' ' x 2;
843            
844 0 0         $rs .= sprintf(" %s:\n %s\n\n", $remark->title, ('=' x (1 + length($remark->title)))) if ($remark->title);
845            
846 0           $rs .= fill($indent, $indent, join("\n", $remark->description))."\n";
847            
848 0           foreach my $link ($remark->links) {
849 0   0       $rs .= sprintf("\n%s* %s (%s)\n", ($indent x 2), $link->href->as_string, ($link->title || $link->rel || '-'));
850             }
851            
852 0           $rs .= "\n";
853             }
854             }
855            
856 0           my @notices = $response->notices;
857 0 0         if (scalar(@notices) > 0) {
858 0           $rs .= "Notices:\n\n";
859 0           foreach my $notice (@notices) {
860 0           my $indent = ' ' x 2;
861            
862 0 0         $rs .= sprintf(" %s:\n %s\n\n", $notice->title, ('=' x (1 + length($notice->title)))) if ($notice->title);
863            
864 0           $rs .= fill($indent, $indent, join("\n", $notice->description))."\n";
865            
866 0           foreach my $link ($notice->links) {
867 0   0       $rs .= sprintf("\n%s* %s (%s)\n", ($indent x 2), $link->href->as_string, ($link->title || $link->rel || '-'));
868             }
869            
870 0           $rs .= "\n";
871             }
872             }
873             }
874              
875 0           return $rs, \@errors;
876             }
877              
878             =head2 rdap_vcard_as_string
879              
880             Format vcard object(s) as string.
881              
882             =cut
883              
884             sub rdap_vcard_as_string {
885 0     0 1   my ($self, $card, $indent) = @_;
886              
887 0           my $vc = "";
888            
889 0 0         $vc .= sprintf("%sName: %s\n\n", $indent, $card->full_name) if ($card->full_name);
890 0 0         $vc .= sprintf("%sOrganization: %s\n\n", $indent, $card->organization) if ($card->organization);
891            
892 0           my @addresses = @{$card->addresses};
  0            
893 0 0         if (scalar(@addresses) > 0) {
894 0           foreach my $address (@addresses) {
895 0           $vc .= sprintf("%sAddress:\n\n", $indent);
896            
897 0           my @lines;
898 0           foreach my $element (@{$address->{'address'}}) {
  0            
899 0 0         push(@lines, ('ARRAY' eq ref($element) ? @{$element} : $element));
  0            
900             }
901            
902 0           $vc .= sprintf $indent." ".join(sprintf("\n%s ", $indent), grep { length > 0 } map { s/^[ \t\r\n]+//g ; s/[ \t\r\n]+$//g ; $_ } @lines)."\n\n";
  0            
  0            
  0            
  0            
903             }
904             }
905            
906 0           foreach my $email (@{$card->email_addresses}) {
  0            
907 0 0         if ($email->{'type'}) {
908 0           $vc .= sprintf("%sEmail: %s (%s)\n\n", $indent, $email->{'address'}, $email->{'type'});
909            
910             } else {
911 0           $vc .= sprintf("%sEmail: %s\n\n", $indent, $email->{'address'});
912            
913             }
914             }
915              
916 0           foreach my $number (@{$card->phones}) {
  0            
917 0 0         my @types = ('ARRAY' eq ref($number->{'type'}) ? @{$number->{'type'}} : ($number->{'type'}));
  0            
918 0 0   0     my $type = ((any { lc($_) eq 'fax' } @types) ? 'Fax' : 'Phone');
  0            
919 0           $vc .= sprintf("%s%s: %s\n\n", $indent, $type, $number->{'number'});
920             }
921            
922 0           return $vc;
923            
924             }
925              
926             =head2 rdap_get_obj_name
927              
928             Fetch the name from an object.
929             If we have a Net::DNS::Domain object (domain/ns), also get the xname.
930              
931             =cut
932              
933             sub rdap_get_obj_name {
934 0     0 1   my ($self, $o) = @_;
935              
936 0           my $xname;
937 0           my $name = $o->name;
938 0 0         return unless $name;
939            
940 0 0         if ('Net::DNS::Domain' eq ref($name)) {
941 0           $xname = $name->xname;
942 0           $name = $name->name;
943             } else {
944 0           $xname = $name;
945             }
946 0           return $name, $xname;
947             }
948              
949             =head2 norid_handle_type
950              
951             Determine type of Norid handle.
952              
953             =cut
954              
955             sub norid_handle_type {
956 0     0 1   my ($self, $handle) = @_;
957              
958 0           $handle = uc($handle);
959            
960 0 0         if ($handle =~ m/REG\d+-NORID$/) {
    0          
961 0           return 'registrar';
962              
963             } elsif ($handle =~ m/.+([O|P|R|H|D])-NORID$/) {
964 0 0         return 'organization' if ($1 eq 'O');
965 0 0         return 'role' if ($1 eq 'R');
966 0 0         return 'person' if ($1 eq 'P');
967 0 0         return 'host' if ($1 eq 'H');
968 0 0         return 'domain' if ($1 eq 'D');
969             }
970 0           die "unknown handle type for: $handle";
971             }
972              
973              
974              
975              
976             =pod
977              
978             =encoding ISO-8859-1
979              
980             =head1 NAME
981              
982             NOLookup::RDAP::RDAPLookup - Lookup RDAP data from the Norid (.no)
983             RDAP service.
984              
985             =head1 SYNOPSIS
986              
987             use Encode;
988             use NOLookup::RDAP::RDAPLookup;
989            
990             # Default API service URL
991             my $SERVICE_URL = "https://rdap.norid.no";
992              
993             # Example 1: Domain name lookup
994             # Decode the query when needed, like for IDNs
995             # or names with national characters.
996              
997             my $q = 'norid.no';
998             #$q = decode('ISO8859-1', 'øl.no');
999              
1000             my $bo = NOLookup::RDAP::RDAPLookup->new(
1001             {
1002             service_url => 'https://rdap.norid.no',
1003             debug => 0,
1004             use_cache => 0,
1005             norid_header_secret => 'secret1234',
1006             norid_header_proxy => 1,
1007             });
1008              
1009             # test HEAD operation for existence
1010             $bo->lookup($q, 1, 0, 0);
1011             if ($bo->error) {
1012             print "HEAD: Error, error / status: ",
1013             $bo->error . "/" . $bo->status) . "\n";
1014             }
1015              
1016             # test GET operations
1017             $bo->lookup($q, 0, 0, 0);
1018             if ($bo->error) {
1019             print "GET: Error, error / status: ",
1020             $bo->error . "/" . $bo->status) . "\n";
1021             }
1022            
1023             # result of lookup is in $bo->result
1024             # This result contains response objects built by Net::RDAP
1025              
1026             my $res = $bo->result;
1027             print "handle: ", $bo->handle, "\n";
1028              
1029             * See bin/no_rdap.pl for more information on usage.
1030              
1031             * See various formatting/helper functions in this file for how to
1032             access the various objects returned by Net::RDAP.
1033              
1034             =head1 DESCRIPTION
1035              
1036             This module provides an object oriented API for use with the
1037             Norid RDAP service. It uses the Net::RDAP module from Cpan
1038             internally to fetch information from the Norid RDAP.
1039              
1040             =head1 SUPPORT
1041              
1042             For now, support questions should be sent to:
1043              
1044             E<lt>(nospam)info(at)norid.noE<gt>
1045              
1046             =head1 SEE ALSO
1047              
1048             L<http://www.norid.no/en>
1049             L<https://www.norid.no/en/registrar/system/tjenester/whois-das-service>
1050             L<https://teknisk.norid.no/en/registrar/system/tjenester/rdap>
1051             =head1 CAVEATS
1052              
1053             =head1 AUTHOR
1054              
1055             Trond Haugen, E<lt>(nospam)info(at)norid.noE<gt>
1056              
1057             =head1 COPYRIGHT
1058              
1059             Copyright (c) 2020- Trond Haugen <(nospam)info(at)norid.no>.
1060             All rights reserved.
1061              
1062             This program is free software; you can redistribute it and/or modify
1063             it under the terms of the GNU General Public License as published by
1064             the Free Software Foundation; either version 2 of the License, or
1065             (at your option) any later version.
1066              
1067             =head1 LICENSE
1068              
1069             This library is free software. You can redistribute it and/or modify
1070             it under the same terms as Perl itself.
1071              
1072             =head1 About the Norid RDAP API
1073              
1074             From Norid doc:
1075              
1076             RDAP is based on a subset of the HTTP protocol. The server accepts
1077             requests of type GET and HEAD. GET lookup is answered with data about
1078             the object in question. HEAD responds if the object exists or
1079             not. Both request types are answered with return code 200 / OK if the object
1080             exists, and return code 404 / NOT FOUND if the object does not exist, and other
1081             return code for other error types.
1082              
1083             The server supports the following types of lookups:
1084              
1085             GET/HEAD https://rdap.norid.no/domain/<domenenavn>
1086             GET/HEAD https://rdap.norid.no/entity/<handle>
1087             GET/HEAD https://rdap.norid.no/registrar/<reg_handle> (Norid extension)
1088             Note: Returns same result as /entity/<reg_handle>
1089             GET/HEAD https://rdap.norid.no/nameserver_handle/<handle> (Norid extension)
1090              
1091             And the following searches:
1092              
1093             GET https://rdap.norid.no/nameservers?name=<hostname>
1094             GET https://rdap.norid.no/domains?identity=<identity> (Norid extension for proxy)
1095             GET https://rdap.norid.no/domains?registrant=<handle> (Norid extension for proxy)
1096             GET https://rdap.norid.no/entities?identity=<identity> (Norid extension for proxy)
1097              
1098             =cut
1099              
1100             1;