├── .github └── workflows │ └── build.yml ├── .gitignore ├── Changes ├── Dockerfile ├── Makefile.PL ├── README.md ├── lib └── App │ └── rdapper.pm ├── rdapper └── t └── 00.use.t /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '22 3 * * sun' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Log in 15 | uses: docker/login-action@v3 16 | with: 17 | username: gbxyz 18 | password: ${{secrets.DOCKER_PASSWORD}} 19 | 20 | - name: Set up QEMU 21 | uses: docker/setup-qemu-action@v3 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | 26 | - name: Build and push 27 | uses: docker/build-push-action@v5 28 | with: 29 | context: . 30 | file: ./Dockerfile 31 | platforms: linux/amd64,linux/arm64 32 | push: true 33 | tags: gbxyz/rdapper:latest 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | App-rdapper-* 2 | Makefile 3 | blib/ 4 | pm_to_blib 5 | MYMETA.* 6 | META.* 7 | MANIFEST 8 | MANIFEST.old 9 | .secrets 10 | .env 11 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | 1.12 - 2025-05-22: 2 | - make --registrar the default behaviour. 3 | 4 | 1.11 - 2025-05-22: 5 | - withdrawn! 6 | 7 | 1.10 - 2025-03-19: 8 | - use Net::RDAP 0.35, which is much faster. 9 | - more consistent underlining of URI-like JCard property values. 10 | 11 | 1.09 - 2025-03-13: 12 | - update this file to reflect the changes in 1.08. 13 | 14 | 1.08 - 2025-03-13: 15 | - require Net::RDAP v0.34 in Makefile.PL not just in rdapper.pm. 16 | 17 | 1.07 - 2025-03-13: 18 | - require Net::RDAP v0.34, which fixes a caching issue. 19 | - add --debug argument, which doesn't do much at the moment except enable Net::RDAP::UA verbosity. 20 | - add --registry argument, as --registrar or --both may become the default behaviour in a future release. 21 | - refactoring of the main display function. 22 | 23 | 1.06 - 2025-02-05: 24 | - Fixed display of JCard property names. 25 | - Display CC parameter of JCard ADR properties (if specified). 26 | - avoid dependency on List::MoreUtils. 27 | - Short output of nameservers when --short is specified. 28 | - Display IPv6 addresses using :: compression instead of the expanded representation. 29 | 30 | 1.05 - 2025-01-06: 31 | - Documentation updates. 32 | - Support for searches. 33 | - Support for --domain (etc) as an alias of --type=domain (etc). 34 | - fix --raw. 35 | - Started to add tests. 36 | 37 | 1.04 - 2024-10-03: 38 | - address security issue due to use of a relative path in `use lib` (thanks Petr Pisar). 39 | 40 | 1.03 - 2024-06-25: 41 | - added --both option, and include all options in --help output. 42 | - simplified implementation of display() using guard clauses 43 | - include additional address fields in output, where present. 44 | 45 | 1.02 - 2024-06-07: 46 | - update to support Net::RDAP 0.25 and display unstructured postal addresses. 47 | 48 | 1.01 - 2024-05-31: 49 | - fix pattern matching which treated TLDs like "de" as IPv6 addresses. 50 | - when an object is identified as an IP address, make sure Net::IP->new returns an object before trying to use it. 51 | - implemented --version argument. 52 | 53 | 1.00 - 2024-05-30: 54 | - bump version to 1.00 as PAUSE ignores 0.1x version numbers as they are lower than 0.9 (:facepalm:) 55 | - display a list of redacted fields where provided 56 | - support TLD queries by directly querying the IANA RDAP server. 57 | 58 | 0.11 - 2024-05-30: 59 | - handle help queries better. 60 | 61 | 0.10 - 2024-05-29: 62 | - add missing dependencies to Makefile.PL. 63 | 64 | 0.9 - 2024-05-29: 65 | - fix copyright notice. 66 | 67 | 0.8 - 2024-05-29: 68 | - when --short argument is used, the EPP status mappings are omitted. 69 | - fix --registrar 70 | - output is nicely wrapped to fit into your terminal. 71 | 72 | 0.7 - 2023-05-18: 73 | - remove unneeded dependency. 74 | - add documentation about Docker. 75 | 76 | 0.6 - 2023-05-18: 77 | - convert to module so it can be distributed via CPAN. 78 | 79 | 0.5 - 2022-10-06: 80 | - this space left intentionally blank. 81 | 82 | 0.4 - 2022-10-06: 83 | - add this file. 84 | - add --registrar argument. 85 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM perl:latest 2 | 3 | RUN cpanm -qn App::rdapper 4 | 5 | ENTRYPOINT ["/usr/local/bin/rdapper"] 6 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | #!perl 2 | use ExtUtils::MakeMaker; 3 | use strict; 4 | 5 | WriteMakefile( 6 | 'NAME' => 'App::rdapper', 7 | 'VERSION_FROM' => 'lib/App/rdapper.pm', 8 | 'ABSTRACT_FROM' => 'lib/App/rdapper.pm', 9 | 'AUTHOR' => [ 'Gavin Brown ' ], 10 | 'LICENSE' => 'perl_5', 11 | 'EXE_FILES' => [ 'rdapper' ], 12 | 'PREREQ_PM' => { 13 | 'Getopt::Long' => 0, 14 | 'JSON' => 0, 15 | 'List::Util' => '1.33', 16 | 'Net::ASN' => 0, 17 | 'Net::DNS::Domain' => 0, 18 | 'Net::IP' => 0, 19 | 'Net::RDAP' => '0.35', 20 | 'Pod::Usage' => 0, 21 | 'Term::ANSIColor' => 0, 22 | 'Term::Size' => 0, 23 | 'Text::Wrap' => 0, 24 | 'URI' => 0, 25 | }, 26 | 'META_MERGE' => { 27 | 'meta-spec' => { 'version' => 2 }, 28 | 'resources' => { 29 | 'repository' => { 30 | 'type' => 'git', 31 | 'url' => 'https://github.com/gbxyz/rdapper.git', 32 | 'web' => 'https://github.com/gbxyz/rdapper', 33 | }, 34 | 'bugtracker' => { 35 | 'web' => 'https://github.com/gbxyz/rdapper/issues', 36 | }, 37 | }, 38 | }, 39 | ); 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | App::rdapper - a simple console-based [RDAP](https://about.rdap.org) client. 4 | 5 | # INSTALLATION 6 | 7 | To install, run: 8 | 9 | cpanm --sudo App::rdapper 10 | 11 | # RUNNING VIA DOCKER 12 | 13 | The [git repository](https://github.com/gbxyz/rdapper) contains a 14 | `Dockerfile` that can be used to build an image on your local system. 15 | 16 | Alternatively, you can pull the [image from Docker 17 | Hub](https://hub.docker.com/r/gbxyz/rdapper): 18 | 19 | $ docker pull gbxyz/rdapper 20 | 21 | $ docker run -it gbxyz/rdapper --help 22 | 23 | # SYNOPSIS 24 | 25 | General form: 26 | 27 | rdapper [OPTIONS] OBJECT 28 | 29 | Examples: 30 | 31 | rdapper example.com 32 | 33 | rdapper --tld foo 34 | 35 | rdapper 192.168.0.1 36 | 37 | rdapper https://rdap.org/domain/example.com 38 | 39 | rdapper --search "exampl*.com" 40 | 41 | # DESCRIPTION 42 | 43 | `rdapper` is a simple RDAP client. It uses [Net::RDAP](https://metacpan.org/pod/Net%3A%3ARDAP) to retrieve data about 44 | internet resources (domain names, IP addresses, and autonymous systems) and 45 | outputs the information in a human-readable format. If you want to consume this 46 | data in your own program you should use [Net::RDAP](https://metacpan.org/pod/Net%3A%3ARDAP) directly. 47 | 48 | # OPTIONS 49 | 50 | You can pass any internet resource as an argument; this may be: 51 | 52 | - a "forward" domain name such as `example.com`; 53 | - a top-level domain such as `com`; 54 | - a IPv4 or IPv6 address or CIDR prefix, such as `192.168.0.1` or 55 | `2001:DB8::/32`; 56 | - an Autonymous System Number such as `AS65536`. 57 | - a "reverse" domain name such as `168.192.in-addr.arpa`; 58 | - the URL of an RDAP resource such as 59 | `https://example.com/rdap/domain/example.com`. 60 | - the "tagged" handle of an entity, such as an LIR, registrar, or domain 61 | admin/tech contact. Because these handles are difficult to distinguish from 62 | domain names, you must use the `--type` argument to explicitly tell 63 | `rdapper` that you want to perform an entity query, .e.g `rdapper 64 | --type=entity ABC123-EXAMPLE`. 65 | 66 | `rdapper` also implements limited support for in-bailiwick nameservers, but 67 | you must use the `--nameserver` argument to disambiguate from domain names. 68 | The RDAP server of the parent domain's registry will be queried. 69 | 70 | ## ARGUMENTS 71 | 72 | - `--registry` - display the registry record only (the default). 73 | - `--registrar` - follow referral to the registrar's RDAP record (if 74 | any) which will be displayed instead of the registry record. Cannot be used with 75 | `--registry`. 76 | - `--both` - display both the registry and (if any) registrar RDAP 77 | records (implies `--registrar`). 78 | - `--reverse` - if you provide an IP address or CIDR prefix, then this 79 | option causes `rdapper` to display the record of the corresponding 80 | `in-addr.arpa` or `ip6.arpa` domain. 81 | - `--type=TYPE` - explicitly set the object type. `rdapper` will 82 | guess the type by pattern matching the value of `OBJECT` but you can override 83 | this by explicitly setting the `--type` argument to one of : `ip`, 84 | `autnum`, `domain`, `nameserver`, `entity` or `url`. 85 | - If `--type=url` is used, `rdapper` will directly fetch the 86 | specified URL and attempt to process it as an RDAP response. If the URL path 87 | ends with `/help` then the response will be treated as a "help" query response 88 | (if you want to see the record for the .help TLD, use `--type=tld help`). 89 | - If `--type=entity` is used, `OBJECT` must be a a string containing 90 | a "tagged" handle, such as `ABC123-EXAMPLE`, as per [RFC 91 | 8521](https://datatracker.ietf.org/doc/html/rfc8521). 92 | - `--$TYPE` - alias for `--type=$TYPE`. eg `--domain`, 93 | `--autnum`, etc. 94 | - `--search` - perform a search. 95 | - `--help` - display help message. 96 | - `--version` - display package and version. 97 | - `--raw` - print the raw JSON rather than parsing it. 98 | - `--short` - omit remarks, notices, links and redactions. 99 | - `--bypass-cache` - disable local cache of RDAP objects. 100 | - `--auth=USER:PASS` - HTTP Basic Authentication credentials to be used 101 | when accessing the specified resource. This option **SHOULD NOT** be used unless 102 | you explicitly specify a URL, otherwise your credentials may be sent to servers 103 | you aren't expecting them to. 104 | - `--nocolor` - disable ANSI colors in the formatted output. 105 | - `--debug` -run in debugging mode. 106 | 107 | # RDAP Search 108 | 109 | Some RDAP servers support the ability to perform simple substring searches. 110 | You can use the `--search` option to enable this functionality. 111 | 112 | When the `--search` option is used, `OBJECT` will be used as a search term. 113 | If it contains no dots (e.g. `exampl*`), then `rdapper` will send a search 114 | query for `exampl*` to _all_ known RDAP servers. If it contains one or more 115 | dots (e.g. `exampl*.com`), it will send the search query to the RDAP server 116 | for the specified TLD (if any). 117 | 118 | Any errors observed will be printed to `STDERR`; any search results will be 119 | printed to `STDOUT`. 120 | 121 | As of writing, search is only available for domain names. 122 | 123 | # COPYRIGHT & LICENSE 124 | 125 | Copyright (c) 2012-2023 CentralNic Ltd. 126 | 127 | Copyright (c) 2023-2025 Gavin Brown. 128 | 129 | All rights reserved. This program is free software; you can redistribute it 130 | and/or modify it under the same terms as Perl itself. 131 | -------------------------------------------------------------------------------- /lib/App/rdapper.pm: -------------------------------------------------------------------------------- 1 | package App::rdapper; 2 | use Getopt::Long qw(GetOptionsFromArray :config pass_through); 3 | use JSON; 4 | use List::Util qw(any min max); 5 | use Net::ASN; 6 | use Net::DNS::Domain; 7 | use Net::IP; 8 | use Net::RDAP::EPPStatusMap; 9 | use Net::RDAP 0.35; 10 | use Pod::Usage; 11 | use Term::ANSIColor; 12 | use Term::Size; 13 | use Text::Wrap; 14 | use URI; 15 | use constant { 16 | # see RFC 6350, Section 6.3.1. 17 | 'ADR_STREET' => 2, 18 | 'ADR_CITY' => 3, 19 | 'ADR_SP' => 4, 20 | 'ADR_PC' => 5, 21 | 'ADR_CC' => 6, 22 | 'INDENT' => ' ', 23 | 'IANA_BASE_URL' => 'https://rdap.iana.org/', 24 | }; 25 | use vars qw($VERSION); 26 | use strict; 27 | 28 | $VERSION = '1.12'; 29 | 30 | # 31 | # global arg variables (note: nopager is now ignored) 32 | # 33 | my ( 34 | $type, $object, $help, $short, $bypass, $auth, $nopager, $raw, $both, 35 | $registrar, $nocolor, $reverse, $version, $search, $debug, $registry 36 | ); 37 | 38 | # 39 | # options spec for Getopt::Long 40 | # 41 | my %opts = ( 42 | 'type:s' => \$type, 43 | 'object:s' => \$object, 44 | 'help' => \$help, 45 | 'short' => \$short, 46 | 'bypass-cache' => \$bypass, 47 | 'auth:s' => \$auth, 48 | 'nopager' => \$nopager, 49 | 'raw' => \$raw, 50 | 'both' => \$both, 51 | 'registrar' => \$registrar, 52 | 'registry' => \$registry, 53 | 'nocolor' => \$nocolor, 54 | 'reverse' => \$reverse, 55 | 'version' => \$version, 56 | 'search' => \$search, 57 | 'debug' => \$debug, 58 | 'autnum' => sub { $type = 'autnum' }, 59 | 'domain' => sub { $type = 'domain' }, 60 | 'entity' => sub { $type = 'entity' }, 61 | 'ip' => sub { $type = 'ip' }, 62 | 'tld' => sub { $type = 'tld' }, 63 | 'url' => sub { $type = 'url' }, 64 | ); 65 | 66 | my $funcs = { 67 | 'ip network' => sub { __PACKAGE__->print_ip(@_) }, 68 | 'autnum' => sub { __PACKAGE__->print_asn(@_) }, 69 | 'domain' => sub { __PACKAGE__->print_domain(@_) }, 70 | 'entity' => sub { __PACKAGE__->print_entity(@_) }, 71 | 'nameserver' => sub { __PACKAGE__->print_nameserver(@_) }, 72 | 'help' => sub { 1 }, # help only contains generic properties 73 | }; 74 | 75 | my @ROLE_DISPLAY_NAMES_ORDER = qw(registrant administrative technical billing 76 | abuse registrar reseller sponsor proxy notifications noc); 77 | 78 | my %ROLE_DISPLAY_NAMES = ('noc' => 'NOC'); 79 | 80 | my @EVENTS = ( 81 | 'registration', 82 | 'reregistration', 83 | 'last changed', 84 | 'expiration', 85 | 'deletion', 86 | 'reinstantiation', 87 | 'transfer', 88 | 'locked', 89 | 'unlocked', 90 | 'last update of RDAP database', 91 | 'registrar expiration', 92 | 'enum validation expiration', 93 | ); 94 | 95 | my %EVENT_DISPLAY_ORDER; 96 | for (my $i = 0 ; $i < scalar(@EVENTS) ; $i++) { 97 | $EVENT_DISPLAY_ORDER{$EVENTS[$i]} = $i; 98 | } 99 | 100 | my @VCARD_DISPLAY_ORDER = qw(SOURCE KIND FN TITLE ROLE ORG ADR GEO EMAIL CONTACT-URI SOCIALPROFILE TEL IMPP URL CATEGORIES NOTE); 101 | my %VCARD_NODE_NAMES = ( 102 | FN => 'Name', 103 | ORG => 'Organization', 104 | TEL => 'Phone', 105 | EMAIL => 'Email', 106 | IMPP => 'Messaging', 107 | URL => 'Website', 108 | SOCIALPROFILE => 'Profile', 109 | 'CONTACT-URI' => 'Contact Link', 110 | GEO => 'Location', 111 | ); 112 | 113 | my @ADR_DISPLAY_ORDER = (ADR_STREET, ADR_CITY, ADR_SP, ADR_PC, ADR_CC); 114 | my %ADR_DISPLAY_NAMES = ( 115 | &ADR_STREET => 'Street', 116 | &ADR_CITY => 'City', 117 | &ADR_SP => 'State/Province', 118 | &ADR_PC => 'Postal Code', 119 | &ADR_CC => 'Country', 120 | ); 121 | 122 | my $json = JSON->new->utf8->canonical->pretty->convert_blessed; 123 | 124 | my $rdap; 125 | 126 | my $out = \*STDOUT; 127 | my $err = \*STDERR; 128 | 129 | $out->binmode(':utf8'); 130 | $err->binmode(':utf8'); 131 | 132 | $Text::Wrap::columns = max((Term::Size::chars)[0], 75); 133 | $Text::Wrap::huge = 'overflow'; 134 | 135 | sub main { 136 | my $package = shift; 137 | 138 | GetOptionsFromArray(\@_, %opts) || $package->show_usage; 139 | 140 | $ENV{NET_RDAP_UA_DEBUG} = 1 if ($debug); 141 | 142 | $rdap = Net::RDAP->new( 143 | 'use_cache' => !$bypass, 144 | 'cache_ttl' => 300, 145 | ); 146 | 147 | $package->show_version if ($version); 148 | 149 | if ($registry && $registrar) { 150 | $package->error("cannot specify both --registry and --registrar, use one or the other."); 151 | 152 | } elsif ($registry && $both) { 153 | $package->error("cannot specify both --registry and --both, use one or the other."); 154 | 155 | } 156 | 157 | $registrar ||= $both; 158 | 159 | if (!$registry && !$both) { 160 | $registrar = 1; 161 | } 162 | 163 | $object = shift(@_) if (!$object); 164 | 165 | $package->show_usage if ($help || length($object) < 1); 166 | 167 | if (!$type) { 168 | if ($object =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) { $type = 'ip' } 169 | elsif ($object =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/) { $type = 'ip' } 170 | elsif ($object =~ /^[0-9a-f:]+:[0-9a-f:]*$/i) { $type = 'ip' } 171 | elsif ($object =~ /^[0-9a-f:]+:[0-9a-f:]*\/\d{1,3}$/i) { $type = 'ip' } 172 | elsif ($object =~ /^asn?\d+$/i) { $type = 'autnum' } 173 | elsif ($object =~ /^(file|https)?:\/\//) { $type = 'url' } 174 | elsif ($object =~ /^([a-z]{2,}|xn--[a-z0-9\-]+)$/i) { $type = 'tld' } 175 | else { $type = 'domain' } 176 | } 177 | 178 | my %args; 179 | ($args{'user'}, $args{'pass'}) = split(/:/, $auth, 2) if ($auth); 180 | 181 | if ($search) { 182 | $package->search($rdap, $object, $type, %args); 183 | 184 | } else { 185 | $package->lookup($rdap, $object, $type, %args); 186 | 187 | } 188 | } 189 | 190 | sub lookup { 191 | my ($package, $rdap, $object, $type, %args) = @_; 192 | 193 | my $response; 194 | 195 | if ('ip' eq $type) { 196 | my $ip = Net::IP->new($object); 197 | 198 | $package->error("invalid IP address '$object'") unless ($ip); 199 | 200 | $response = $rdap->ip($ip, %args); 201 | 202 | $response = $rdap->fetch($response->domain) if ($reverse); 203 | 204 | } elsif ('autnum' eq $type) { 205 | my $asn = $object; 206 | $asn =~ s/^asn?//ig; 207 | 208 | $response = $rdap->autnum(Net::ASN->new($asn), %args); 209 | 210 | } elsif ('domain' eq $type) { 211 | $response = $rdap->domain(Net::DNS::Domain->new($object), %args); 212 | 213 | } elsif ('nameserver' eq $type) { 214 | my $url = Net::RDAP::Registry->get_url(Net::DNS::Domain->new($object)); 215 | 216 | # 217 | # munge path 218 | # 219 | my $path = $url->path; 220 | $path =~ s!/domain/!/nameserver/!; 221 | $url->path($path); 222 | 223 | $response = $rdap->fetch($url, %args); 224 | 225 | } elsif ('entity' eq $type) { 226 | $response = $rdap->entity($object, %args); 227 | 228 | } elsif ('tld' eq $type) { 229 | $response = $rdap->fetch(URI->new(IANA_BASE_URL.'domain/'.$object), %args); 230 | 231 | } elsif ('url' eq $type) { 232 | my $uri = URI->new($object); 233 | 234 | # 235 | # if the path ends with /help then we assume then it's a help query 236 | # 237 | $args{'class_override'} = 'help' if ('help' eq lc(($uri->path_segments)[-1])); 238 | 239 | $response = $rdap->fetch($uri, %args); 240 | 241 | } else { 242 | $package->error("Unable to handle type '$type'"); 243 | 244 | } 245 | 246 | $package->display($response, 0); 247 | } 248 | 249 | sub show_usage { 250 | my $package = shift; 251 | 252 | pod2usage( 253 | '-input' => __FILE__, 254 | '-verbose' => 99, 255 | '-sections' => [qw(SYNOPSIS OPTIONS)], 256 | ); 257 | } 258 | 259 | sub show_version { 260 | my $package = shift; 261 | $out->say(sprintf('%s v%s', $package, $VERSION)); 262 | exit; 263 | } 264 | 265 | sub search { 266 | my ($package, $rdap, $object, $type, %args) = @_; 267 | 268 | if ('domain' eq $type) { 269 | $package->domain_search($rdap, $object, %args); 270 | 271 | } else { 272 | $package->error('current unable to do searches for %s objects.', $type); 273 | 274 | } 275 | } 276 | 277 | sub domain_search { 278 | my ($package, $rdap, $query, %args) = @_; 279 | 280 | my @labels = grep { length > 0 } split(/\./, lc($query), 2); 281 | 282 | my $prefix = shift(@labels); 283 | my $suffix = shift(@labels) || '*'; 284 | 285 | my $servers = {}; 286 | my $zones = {}; 287 | 288 | foreach my $service (Net::RDAP::Registry->load_registry(Net::RDAP::Registry::DNS_URL)->services) { 289 | foreach my $zone ($service->registries) { 290 | my $url = Net::RDAP::Registry->get_best_url($service->urls); 291 | 292 | if (!exists($servers->{$url->as_string})) { 293 | $servers->{$url->as_string} = Net::RDAP::Service->new($url); 294 | } 295 | 296 | $zones->{lc($zone)} = $url->as_string; 297 | } 298 | } 299 | 300 | my @zones = sort(keys(%{$zones})); 301 | @zones = grep { lc($suffix) eq $_ || $suffix =~ /\.$_/i } @zones if ($suffix ne '*'); 302 | 303 | foreach my $zone (@zones) { 304 | my $server = $servers->{$zones->{$zone}}; 305 | my $result = $server->domains(name => $prefix); 306 | 307 | if ($result->isa('Net::RDAP::Error')) { 308 | $package->warning(sprintf('%s.%s: %s %s', $prefix, $zone, $result->errorCode, $result->title)); 309 | 310 | } elsif ($result->isa('Net::RDAP::SearchResult')) { 311 | $package->display_domain_search_results($result); 312 | 313 | } 314 | } 315 | } 316 | 317 | sub display_domain_search_results { 318 | my ($package, $result) = @_; 319 | 320 | foreach my $domain ($result->domains) { 321 | $out->say($domain->name->name); 322 | } 323 | } 324 | 325 | sub display_nameserver_search_results { 326 | my ($package, $result) = @_; 327 | 328 | foreach my $nameserver ($result->nameservers) { 329 | $out->say($nameserver->name->name); 330 | } 331 | } 332 | 333 | sub display_entity_search_results { 334 | my ($package, $result) = @_; 335 | 336 | foreach my $entity ($result->entities) { 337 | $out->say($entity->handle); 338 | } 339 | } 340 | 341 | sub display_search { 342 | my ($package, $result) = @_; 343 | 344 | $package->display_domain_search_results($result) if (exists($result->{domainSearchResults})); 345 | $package->display_nameserver_search_results($result) if (exists($result->{nameserverSearchResults})); 346 | $package->display_entity_search_results($result) if (exists($result->{entitySearchResults})); 347 | } 348 | 349 | sub display { 350 | my ($package, $object, $indent, $nofatal) = @_; 351 | 352 | if ($object->isa('Net::RDAP::Error')) { 353 | if ($nofatal) { 354 | $package->warning('%03u (%s)', $object->errorCode, $object->title); 355 | 356 | } else { 357 | $package->error('%03u (%s)', $object->errorCode, $object->title); 358 | 359 | } 360 | 361 | } else { 362 | my $link = (grep { 'related' eq $_->rel && $_->is_rdap } $object->links)[0]; 363 | 364 | if ($registrar) { 365 | # avoid recursing infinitely 366 | $registrar = undef; 367 | 368 | if (!$link) { 369 | $package->display($object, $indent); 370 | 371 | } else { 372 | my $result = $rdap->fetch($link); 373 | 374 | if ($result->isa('Net::RDAP::Error')) { 375 | $package->display($result, $indent, 1); 376 | 377 | $package->warning('Unable to retrieve registrar record, displaying the registry record...'); 378 | $package->display($object, $indent); 379 | 380 | } else { 381 | $package->display($object, $indent, 1) if ($both); 382 | 383 | $package->display($result, $indent); 384 | 385 | } 386 | } 387 | 388 | } else { 389 | if ($raw) { 390 | $out->print($json->encode($object)); 391 | 392 | } elsif ($object->isa('Net::RDAP::SearchResult')) { 393 | $package->display_search($object); 394 | 395 | } else { 396 | $package->display_object($object, $indent); 397 | 398 | } 399 | } 400 | } 401 | } 402 | 403 | sub display_object { 404 | my ($package, $object, $indent) = @_; 405 | 406 | $package->error("object does not include the 'objectClassName' properties") unless ($object->class); 407 | $package->error(sprintf("unknown object type '%s'", $object->class)) unless ($funcs->{$object->class}); 408 | 409 | # 410 | # generic properties 411 | # 412 | $package->print_kv('Object type', $object->class, $indent) if ($indent < 1); 413 | $package->print_kv('URL', u($object->self->href), $indent) if ($indent < 1 && $object->self); 414 | 415 | if ($object->can('name')) { 416 | my $name = $object->name; 417 | 418 | if ($name) { 419 | my $xname; 420 | 421 | if ($name->isa('Net::DNS::Domain')) { 422 | $xname = $name->xname; 423 | $name = $name->name; 424 | 425 | } else { 426 | $xname = $name; 427 | 428 | } 429 | 430 | if ($xname ne $name) { 431 | $package->print_kv('Name', sprintf('%s (%s)', uc($xname), uc($name))); 432 | 433 | } else { 434 | $package->print_kv('Name', uc($name)); 435 | 436 | } 437 | } 438 | } 439 | 440 | # 441 | # object-specific properties 442 | # 443 | $funcs->{$object->class}->($object, $indent); 444 | 445 | # 446 | # more generic properties 447 | # 448 | $package->print_events($object, $indent); 449 | $package->print_status($object, $indent, ('domain' eq $object->class)); 450 | 451 | $package->print_entities($object, $indent); 452 | 453 | # 454 | # links, remarks, notices and redactions, unless --short has been passed 455 | # 456 | if (!$short) { 457 | foreach my $link (grep { 'self' ne $_->rel } $object->links) { 458 | $package->print_link($link, $indent); 459 | } 460 | 461 | foreach my $remark ($object->remarks) { 462 | $package->print_remark_or_notice($remark, $indent); 463 | } 464 | 465 | foreach my $notice ($object->notices) { 466 | $package->print_remark_or_notice($notice, $indent); 467 | } 468 | 469 | my @fields = $object->redactions; 470 | if (scalar(@fields) > 0) { 471 | $package->print_kv('Redacted Fields', '', $indent); 472 | foreach my $field (@fields) { 473 | $out->print(wrap( 474 | (INDENT x ($indent + 1)), 475 | (INDENT x ($indent + 2)), 476 | sprintf("%s %s (reason: %s)\n", b('*'), $field->name, $field->reason) 477 | )); 478 | } 479 | } 480 | } 481 | 482 | $out->print("\n") if ($indent < 1); 483 | } 484 | 485 | sub print_ip { 486 | my ($package, $ip, $indent) = @_; 487 | 488 | $package->print_kv('Handle', $ip->handle, $indent) if ($ip->handle); 489 | $package->print_kv('Version', $ip->version, $indent) if ($ip->version); 490 | $package->print_kv('Domain', u($ip->domain->as_string), $indent) if ($ip->domain); 491 | $package->print_kv('Type', $ip->type, $indent) if ($ip->type); 492 | $package->print_kv('Country', $ip->country, $indent) if ($ip->country); 493 | $package->print_kv('Parent', $ip->parentHandle, $indent) if ($ip->parentHandle); 494 | $package->print_kv('Range', $ip->range->prefix, $indent) if ($ip->range); 495 | 496 | foreach my $cidr ($ip->cidrs) { 497 | $package->print_kv('CIDR', $cidr->prefix, $indent); 498 | } 499 | } 500 | 501 | sub print_asn { 502 | my ($package, $asn, $indent) = @_; 503 | 504 | $package->print_kv('Handle', $asn->handle, $indent) if ($asn->handle); 505 | $package->print_kv('Range', sprintf('%u - %u', $asn->start, $asn->end), $indent) if ($asn->start > 0 && $asn->end > 0 && $asn->end > $asn->start); 506 | $package->print_kv('Type', $asn->type, $indent) if ($asn->type); 507 | } 508 | 509 | sub print_domain { 510 | my ($package, $domain, $indent) = @_; 511 | 512 | $package->print_kv('Handle', $domain->handle, $indent) if ($domain->handle); 513 | 514 | foreach my $ns (sort { lc($a->name->name) cmp lc($b->name->name) } $domain->nameservers) { 515 | if ($short) { 516 | $package->print_kv('Nameserver', uc($ns->name->name) . ' ' . join(' ', map { $_->short } $ns->addresses), $indent); 517 | 518 | } else { 519 | $package->print_kv('Nameserver', uc($ns->name->name), $indent); 520 | $package->print_nameserver($ns, 1+$indent); 521 | } 522 | } 523 | 524 | foreach my $ds ($domain->ds) { 525 | $package->print_kv('DS Record', $ds->plain, $indent); 526 | } 527 | 528 | foreach my $key ($domain->keys) { 529 | $package->print_kv('DNSKEY Record', $key->plain, $indent); 530 | } 531 | 532 | $package->display_artRecord($domain->{'artRecord_record'}, $indent) if ($domain->{'artRecord_record'}); 533 | $package->display_platform_nameservers($domain->{'platformNS_nameservers'}, $indent) if ($domain->{'platformNS_nameservers'}); 534 | 535 | $package->print_kv('Registration Type', $domain->{'regType_regType'}) if ($domain->{'regType_regType'}); 536 | } 537 | 538 | sub display_artRecord { 539 | my ($package, $records, $indent) = @_; 540 | 541 | $package->print_kv('Art Record', undef, $indent); 542 | 543 | foreach my $record (@{$records}) { 544 | $package->print_kv($record->{'name'}, $record->{'value'}, 1+$indent); 545 | } 546 | } 547 | 548 | sub display_platform_nameservers { 549 | my ($package, $nameservers, $indent) = @_; 550 | 551 | foreach my $ns (@{$nameservers}) { 552 | $package->print_kv('Platform Nameserver', uc(Net::RDAP::Object::Nameserver->new($ns)->name->name), $indent); 553 | } 554 | } 555 | 556 | sub print_entity { 557 | my ($package, $entity, $indent) = @_; 558 | 559 | $package->print_kv('Handle', $entity->handle, $indent) if ($entity->handle && $indent < 1); 560 | 561 | foreach my $id ($entity->ids) { 562 | $package->print_kv($id->type, $id->identifier, $indent); 563 | } 564 | 565 | my $jcard = $entity->jcard; 566 | if ($jcard) { 567 | $package->print_jcard($jcard, $indent); 568 | } 569 | } 570 | 571 | sub print_jcard { 572 | my ($package, $jcard, $indent) = @_; 573 | 574 | foreach my $ptype (@VCARD_DISPLAY_ORDER) { 575 | foreach my $property (grep { $_->value } $jcard->properties($ptype)) { 576 | $package->print_jcard_property($property, $indent); 577 | } 578 | } 579 | } 580 | 581 | sub print_jcard_property { 582 | my ($package, $property, $indent) = @_; 583 | 584 | if ('ADR' eq uc($property->type)) { 585 | $package->print_jcard_adr($property, $indent); 586 | 587 | } else { 588 | my $label = $VCARD_NODE_NAMES{uc($property->type)} || ucfirst(lc($property->type)); 589 | 590 | if ('TEL' eq uc($property->type)) { 591 | if (any { 'fax' eq lc($_) } @{$property->param('type')}) { 592 | $label = 'Fax'; 593 | 594 | } else { 595 | $label = 'Phone'; 596 | 597 | } 598 | } 599 | 600 | $package->print_kv( 601 | $label, 602 | $property->may_be_uri ? u($property->value) : $property->value, 603 | $indent 604 | ); 605 | } 606 | } 607 | 608 | sub print_jcard_adr { 609 | my ($package, $property, $indent) = @_; 610 | 611 | $package->print_kv('Address', '', $indent); 612 | 613 | if ($property->param('label')) { 614 | $out->print(wrap( 615 | INDENT x ($indent + 1), 616 | INDENT x ($indent + 1), 617 | $property->param('label'), 618 | )."\n"); 619 | 620 | } else { 621 | foreach my $i (@ADR_DISPLAY_ORDER) { 622 | if ($property->value->[$i]) { 623 | if ('ARRAY' eq ref($property->value->[$i])) { 624 | foreach my $v (grep { $_ } @{$property->value->[$i]}) { 625 | $package->print_kv($ADR_DISPLAY_NAMES{$i}, $v, $indent+1); 626 | } 627 | 628 | } else { 629 | $package->print_kv($ADR_DISPLAY_NAMES{$i}, $property->value->[$i], $indent+1); 630 | 631 | } 632 | } 633 | } 634 | } 635 | 636 | if ($property->param('cc')) { 637 | $package->print_kv('Country', $property->param('cc'), $indent+1); 638 | } 639 | } 640 | 641 | sub print_nameserver { 642 | my ($package, $nameserver, $indent) = @_; 643 | 644 | $package->print_kv('Handle', $nameserver->handle, $indent) if ($nameserver->handle); 645 | 646 | foreach my $ip ($nameserver->addresses) { 647 | $package->print_kv('IP Address', $ip->short, $indent); 648 | } 649 | } 650 | 651 | sub print_events { 652 | my ($package, $object, $indent) = @_; 653 | 654 | foreach my $event (sort { $EVENT_DISPLAY_ORDER{$a->action} - $EVENT_DISPLAY_ORDER{$b->action} } $object->events) { 655 | if ($event->actor) { 656 | $package->print_kv(ucfirst($event->action), sprintf('%s (by %s)', scalar($event->date), $event->actor), $indent); 657 | 658 | } else { 659 | $package->print_kv(ucfirst($event->action), scalar($event->date).$event->date_tz, $indent); 660 | 661 | } 662 | } 663 | } 664 | 665 | sub print_status { 666 | my ($package, $object, $indent, $is_domain) = @_; 667 | 668 | foreach my $status ($object->status) { 669 | my $epp = rdap2epp($status); 670 | if ($epp && $is_domain && !$short) { 671 | $package->print_kv('Status', sprintf('%s (EPP: %s, %s)', $status, $epp, u(sprintf('https://icann.org/epp#%s', $epp))), $indent); 672 | 673 | } else { 674 | $package->print_kv('Status', $status, $indent); 675 | 676 | } 677 | } 678 | } 679 | 680 | sub print_entities { 681 | my ($package, $object, $indent) = @_; 682 | 683 | my @entities = $object->entities; 684 | 685 | my %seen; 686 | foreach my $role (@ROLE_DISPLAY_NAMES_ORDER) { 687 | for (my $i = 0 ; $i < scalar(@entities) ; $i++) { 688 | next if ($seen{$i}); 689 | 690 | my $entity = $entities[$i]; 691 | if (any { $role eq $_ } $entity->roles) { 692 | $seen{$i} = 1; 693 | 694 | my $rstring = join(', ', map { sprintf('%s Contact', $ROLE_DISPLAY_NAMES{$_} || ucfirst($_)) } $entity->roles); 695 | 696 | if ($entity->handle && 'not applicable' ne $entity->handle && 'HANDLE REDACTED FOR PRIVACY' ne $entity->handle) { 697 | $package->print_kv($rstring, $entity->handle, $indent); 698 | 699 | } else { 700 | $package->print_kv($rstring, undef, $indent); 701 | 702 | } 703 | 704 | eval { 705 | $package->display($entity, 1+$indent, 1); 706 | }; 707 | } 708 | } 709 | } 710 | } 711 | 712 | sub print_remark_or_notice { 713 | my ($package, $thing, $indent) = @_; 714 | 715 | my $type = ($thing->isa('Net::RDAP::Notice') ? 'Notice' : 'Remark'); 716 | 717 | if (1 == scalar($thing->description)) { 718 | $package->print_kv($thing->title || $type, ($thing->description)[0], $indent); 719 | 720 | } else { 721 | $package->print_kv($thing->title || $type, , '', $indent); 722 | 723 | $out->print(fill( 724 | (INDENT x (1+$indent)), 725 | (INDENT x (1+$indent)), 726 | $thing->description 727 | )."\n"); 728 | } 729 | 730 | foreach my $link ($thing->links) { 731 | $package->print_link($link, 1+$indent); 732 | } 733 | } 734 | 735 | sub print_link { 736 | my ($package, $link, $indent) = @_; 737 | 738 | $package->print_kv( 739 | $link->title || ('related' eq $link->rel ? 'Link' : ucfirst($link->rel)) || 'Link', 740 | u($link->href->as_string), 741 | $indent, 742 | ); 743 | } 744 | 745 | sub print_kv { 746 | my ($package, $name, $value, $indent) = @_; 747 | 748 | $out->print(wrap( 749 | (INDENT x $indent), 750 | (INDENT x ($indent + 1)), 751 | sprintf("%s %s\n", b($name.':'), $value), 752 | )); 753 | } 754 | 755 | sub debug { 756 | my ($package, $fmt, @params) = @_; 757 | if ($debug) { 758 | my $str = sprintf("Debug: $fmt", @params); 759 | $err->say(colourise([qw(magenta)], $str)); 760 | } 761 | } 762 | 763 | sub info { 764 | my ($package, $fmt, @params) = @_; 765 | my $str = sprintf("Info: $fmt", @params); 766 | $err->say(colourise([qw(cyan)], $str)); 767 | } 768 | 769 | sub warning { 770 | my ($package, $fmt, @params) = @_; 771 | my $str = sprintf("Warning: $fmt", @params); 772 | $err->say(colourise([qw(yellow)], $str)); 773 | } 774 | 775 | sub error { 776 | my ($package, $fmt, @params) = @_; 777 | my $str = sprintf("Error: $fmt", @params); 778 | $err->say(colourise([qw(red)], $str)); 779 | exit 1; 780 | } 781 | 782 | sub colourise { 783 | my ($cref, $str) = @_; 784 | 785 | if (-t $out && !$nocolor) { 786 | return colored($cref, $str); 787 | 788 | } else { 789 | return $str; 790 | 791 | } 792 | } 793 | 794 | sub u { colourise([qw(underline)], shift) } 795 | sub b { colourise([qw(bold)], shift) } 796 | 797 | 1; 798 | 799 | __END__ 800 | 801 | =pod 802 | 803 | =head1 NAME 804 | 805 | App::rdapper - a simple console-based L client. 806 | 807 | =head1 INSTALLATION 808 | 809 | To install, run: 810 | 811 | cpanm --sudo App::rdapper 812 | 813 | =head1 RUNNING VIA DOCKER 814 | 815 | The L contains a 816 | C that can be used to build an image on your local system. 817 | 818 | Alternatively, you can pull the L: 820 | 821 | $ docker pull gbxyz/rdapper 822 | 823 | $ docker run -it gbxyz/rdapper --help 824 | 825 | =head1 SYNOPSIS 826 | 827 | General form: 828 | 829 | rdapper [OPTIONS] OBJECT 830 | 831 | Examples: 832 | 833 | rdapper example.com 834 | 835 | rdapper --tld foo 836 | 837 | rdapper 192.168.0.1 838 | 839 | rdapper https://rdap.org/domain/example.com 840 | 841 | rdapper --search "exampl*.com" 842 | 843 | =head1 DESCRIPTION 844 | 845 | C is a simple RDAP client. It uses L to retrieve data about 846 | internet resources (domain names, IP addresses, and autonymous systems) and 847 | outputs the information in a human-readable format. If you want to consume this 848 | data in your own program you should use L directly. 849 | 850 | =head1 OPTIONS 851 | 852 | You can pass any internet resource as an argument; this may be: 853 | 854 | =over 855 | 856 | =item * a "forward" domain name such as C; 857 | 858 | =item * a top-level domain such as C; 859 | 860 | =item * a IPv4 or IPv6 address or CIDR prefix, such as C<192.168.0.1> or 861 | C<2001:DB8::/32>; 862 | 863 | =item * an Autonymous System Number such as C. 864 | 865 | =item * a "reverse" domain name such as C<168.192.in-addr.arpa>; 866 | 867 | =item * the URL of an RDAP resource such as 868 | C. 869 | 870 | =item * the "tagged" handle of an entity, such as an LIR, registrar, or domain 871 | admin/tech contact. Because these handles are difficult to distinguish from 872 | domain names, you must use the C<--type> argument to explicitly tell 873 | C that you want to perform an entity query, .e.g C. 875 | 876 | =back 877 | 878 | C also implements limited support for in-bailiwick nameservers, but 879 | you must use the C<--nameserver> argument to disambiguate from domain names. 880 | The RDAP server of the parent domain's registry will be queried. 881 | 882 | =head2 ARGUMENTS 883 | 884 | =over 885 | 886 | =item * C<--registry> - display the registry record only. This was the default 887 | behaviour prior to v1.12. 888 | 889 | =item * C<--registrar> - follow referral to the registrar's RDAP record (if 890 | any) which will be displayed instead of the registry record. If no registrar 891 | link can be found, the registry record will be displayed. This option cannot be 892 | used with C<--registry>. As of v1.12, this is the default behaviour. 893 | 894 | =item * C<--both> - display both the registry and (if any) registrar RDAP 895 | records. 896 | 897 | =item * C<--reverse> - if you provide an IP address or CIDR prefix, then this 898 | option causes C to display the record of the corresponding 899 | C or C domain. 900 | 901 | =item * C<--type=TYPE> - explicitly set the object type. C will 902 | guess the type by pattern matching the value of C but you can override 903 | this by explicitly setting the C<--type> argument to one of : C, 904 | C, C, C, C or C. 905 | 906 | =over 907 | 908 | =item * If C<--type=url> is used, C will directly fetch the 909 | specified URL and attempt to process it as an RDAP response. If the URL path 910 | ends with C then the response will be treated as a "help" query response 911 | (if you want to see the record for the .help TLD, use C<--type=tld help>). 912 | 913 | =item * If C<--type=entity> is used, C must be a a string containing 914 | a "tagged" handle, such as C, as per L. 916 | 917 | =back 918 | 919 | =item * C<--$TYPE> - alias for C<--type=$TYPE>. eg C<--domain>, 920 | C<--autnum>, etc. 921 | 922 | =item * C<--search> - perform a search. 923 | 924 | =item * C<--help> - display help message. 925 | 926 | =item * C<--version> - display package and version. 927 | 928 | =item * C<--raw> - print the raw JSON rather than parsing it. 929 | 930 | =item * C<--short> - omit remarks, notices, links and redactions. 931 | 932 | =item * C<--bypass-cache> - disable local cache of RDAP objects. 933 | 934 | =item * C<--auth=USER:PASS> - HTTP Basic Authentication credentials to be used 935 | when accessing the specified resource. This option B be used unless 936 | you explicitly specify a URL, otherwise your credentials may be sent to servers 937 | you aren't expecting them to. 938 | 939 | =item * C<--nocolor> - disable ANSI colors in the formatted output. 940 | 941 | =item * C<--debug> -run in debugging mode. 942 | 943 | =back 944 | 945 | =head1 RDAP Search 946 | 947 | Some RDAP servers support the ability to perform simple substring searches. You 948 | can use the C<--search> option to enable this functionality. 949 | 950 | When the C<--search> option is used, C will be used as a search term. 951 | If it contains no dots (e.g. C), then C will send a search 952 | query for C to I known RDAP servers. If it contains one or more 953 | dots (e.g. C), it will send the search query to the RDAP server 954 | for the specified TLD (if any). 955 | 956 | Any errors observed will be printed to C; any search results will be 957 | printed to C. 958 | 959 | As of writing, search is only available for domain names. 960 | 961 | =head1 COPYRIGHT & LICENSE 962 | 963 | Copyright (c) 2012-2023 CentralNic Ltd. 964 | 965 | Copyright (c) 2023-2025 Gavin Brown. 966 | 967 | All rights reserved. This program is free software; you can redistribute it 968 | and/or modify it under the same terms as Perl itself. 969 | 970 | =cut 971 | -------------------------------------------------------------------------------- /rdapper: -------------------------------------------------------------------------------- 1 | #!perl 2 | use App::rdapper; 3 | use strict; 4 | 5 | eval { 6 | App::rdapper->main(@ARGV); 7 | exit(0); 8 | }; 9 | 10 | exit(1); 11 | -------------------------------------------------------------------------------- /t/00.use.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use Test::More; 3 | use strict; 4 | 5 | require_ok 'App::rdapper'; 6 | 7 | done_testing; 8 | --------------------------------------------------------------------------------