Skip to content
ipdataPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

24 Commits

Folders and files

Repository files navigation

Net::IPData

Perl client for the ipdata.co IP geolocation and threat intelligence API.

Features

  • Full API coverage -- single IP lookup, bulk lookup (up to 100 IPs), field filtering, single-field queries
  • Threat intelligence -- TOR, proxy, datacenter, VPN, and botnet detection
  • Rich data -- geolocation, ASN, company, carrier, currency, timezone, and language info
  • EU endpoint -- route requests through EU-only servers for GDPR compliance
  • Zero non-core dependencies -- uses only HTTP::Tiny and JSON::PP (core since Perl 5.14)
  • Connection reuse -- one persistent HTTPS connection per client object (HTTP/1.1 keep-alive), so only the first request pays the TLS handshake
  • Certificate verification -- the API server's TLS certificate is verified with every HTTP::Tiny version (verify_ssl => 0 turns it off)
  • Production ready -- input validation, structured error handling, configurable timeouts

Installation

From CPAN:

cpanm Net::IPData

Or manually:

perl Makefile.PL
make
make test
make install

Quick Start

use Net::IPData;

my $ipdata = Net::IPData->new(api_key => 'YOUR_API_KEY');

my $result = $ipdata->lookup('8.8.8.8');
printf "%s is in %s, %s\n",
    $result->{ip},
    $result->{city},
    $result->{country_name};

Get a free API key (1,500 requests/day) at ipdata.co.

Usage

Single IP Lookup

my $result = $ipdata->lookup('8.8.8.8');

printf "IP:        %s\n", $result->{ip};
printf "City:      %s\n", $result->{city};
printf "Region:    %s\n", $result->{region};
printf "Country:   %s (%s)\n", $result->{country_name}, $result->{country_code};
printf "Continent: %s\n", $result->{continent_name};
printf "Coords:    %.4f, %.4f\n", $result->{latitude}, $result->{longitude};
printf "ASN:       %s (%s)\n", $result->{asn}{asn}, $result->{asn}{name};
printf "Timezone:  %s\n", $result->{time_zone}{name};
printf "Currency:  %s (%s)\n", $result->{currency}{name}, $result->{currency}{code};
printf "EU:        %s\n", $result->{is_eu} ? 'Yes' : 'No';
printf "Threat:    %s\n", $result->{threat}{is_threat} ? 'Yes' : 'No';
printf "Satellite: %s\n", $result->{is_satellite} ? 'Yes' : 'No';   # key present only when true
printf "Anycast:   %s\n", $result->{is_anycast} ? 'Yes' : 'No';     # key present only when true

Look Up Your Own IP

my $me = $ipdata->lookup_mine();
printf "My IP: %s\n", $me->{ip};

Field Filtering

Request only the fields you need to reduce response size and improve performance:

my $partial = $ipdata->lookup('8.8.8.8', fields => [qw(ip country_name asn)]);
printf "Country: %s, ASN: %s\n",
    $partial->{country_name},
    $partial->{asn}{name};

Fields can also be passed as a comma-separated string:

my $partial = $ipdata->lookup('8.8.8.8', fields => 'ip,country_name,asn');

Bulk Lookup

Look up to 100 IP addresses in a single request:

my $results = $ipdata->bulk(['8.8.8.8', '1.1.1.1', '9.9.9.9']);

for my $r (@$results) {
    printf "%s -> %s, %s\n", $r->{ip}, $r->{city}, $r->{country_name};
}

# With field filtering
my $results = $ipdata->bulk(
    ['8.8.8.8', '1.1.1.1'],
    fields => [qw(ip country_name threat)],
);

Single-Field Shortcuts

Retrieve individual fields directly without downloading the full response:

my $asn     = $ipdata->asn('8.8.8.8');          # hashref
my $threat  = $ipdata->threat('8.8.8.8');        # hashref
my $carrier = $ipdata->carrier('8.8.8.8');       # hashref
my $tz      = $ipdata->time_zone('8.8.8.8');     # hashref
my $curr    = $ipdata->currency('8.8.8.8');      # hashref
my $langs   = $ipdata->languages('8.8.8.8');     # arrayref

my $country = $ipdata->country_name('8.8.8.8');  # string
my $code    = $ipdata->country_code('8.8.8.8');  # string
my $eu      = $ipdata->is_eu('8.8.8.8');         # boolean
my $sat     = $ipdata->is_satellite('8.8.8.8');  # boolean
my $anycast = $ipdata->is_anycast('8.8.8.8');    # boolean

# Or use the generic method for any path-accessible field
my $city = $ipdata->lookup_field('8.8.8.8', 'city');

EU Endpoint

Route all requests through EU-only servers (Paris, Ireland, Frankfurt) for GDPR compliance:

my $ipdata = Net::IPData->new(
    api_key => 'YOUR_API_KEY',
    eu      => 1,
);

Custom Base URL

my $ipdata = Net::IPData->new(
    api_key  => 'YOUR_API_KEY',
    base_url => 'https://custom-proxy.example.com',
);

Error Handling

All methods croak on errors. Use eval or Try::Tiny to catch them:

use Try::Tiny;

try {
    my $result = $ipdata->lookup('invalid');
} catch {
    warn "API error: $_";
};

Error messages include the HTTP status code and the API's error message:

Status Meaning
401 Invalid or missing API key
403 Access forbidden
429 Rate limit exceeded
599 Network/connection failure

Request Headers

Every request carries Accept: application/json and the user agent ipdata-perl/<version>. The bulk POST -- the only request with a body -- also sends Content-Type: application/json; GET requests send no Content-Type.

Connection reuse

A Net::IPData object keeps one persistent HTTPS connection (HTTP/1.1 keep-alive) for as long as it lives. Create one client per process (or one per API key or endpoint), keep it, and reuse it for every lookup: each new client pays a fresh TLS handshake. HTTP/2 is not available with HTTP::Tiny, so the saving is the TLS handshake, not multiplexing.

my $ipdata = Net::IPData->new(api_key => 'YOUR_API_KEY');   # one TLS handshake...

for my $ip (@ips) {
    my $result = $ipdata->lookup($ip);                       # ...reused by every lookup
}

The reuse is HTTP::Tiny's own (keep_alive => 1, now explicit and configurable): a connection the server has already closed is detected and replaced transparently, a GET on a socket that died silently is retried once by HTTP::Tiny itself, and bulk (a non-idempotent POST) gets the same single retry from this module when the failure happened at the socket level before any response arrived (HTTP::Tiny status 599 with Socket closed, Unexpected end of stream or Connection reset). A request that received an HTTP status is never retried. With HTTP::Tiny 0.092 or later, keep_alive_timeout => 60 additionally stops reusing a connection that has been idle for a minute, before the server (which keeps idle connections for roughly two minutes) can drop it silently.

A Net::IPData object is not shared between threads: use one per thread. It is fork-safe: HTTP::Tiny 0.049 and later never reuses a socket inherited from another process or thread, so a forked child opens its own connection on its first request. There is nothing to close explicitly; the connection is shut down when the object is destroyed, so letting it go out of scope (or undef $ipdata) at shutdown is all the cleanup needed.

Pass keep_alive => 0 to open a new connection per request, or supply your own user agent with ua => $obj (any object with HTTP::Tiny-compatible get/post methods returning {status, content, headers, success}), for example a pre-configured HTTP::Tiny or a mock in tests:

my $ua     = HTTP::Tiny->new(
    keep_alive  => 1,
    timeout     => 10,
    SSL_options => { SSL_ca_file => '/etc/ssl/private-ca.pem' },
);
my $ipdata = Net::IPData->new(api_key => 'YOUR_API_KEY', ua => $ua);

Constructor Options

Parameter Type Default Description
api_key string required Your ipdata.co API key
eu bool 0 Use the EU endpoint (eu-api.ipdata.co)
base_url string https://api.ipdata.co Custom base URL (overrides eu)
timeout int 30 HTTP timeout in seconds
keep_alive bool 1 Keep the HTTPS connection open between requests (HTTP/1.1 keep-alive); 0 opens a new connection per request
keep_alive_timeout int unset Seconds a kept-alive connection may sit idle before the next request opens a fresh one; honoured by HTTP::Tiny 0.092+ (older versions ignore it) and forwarded only when set; 60 suits api.ipdata.co
verify_ssl bool 1 Verify the API server's TLS certificate. Passed to HTTP::Tiny as verify_SSL, so the check applies with every HTTP::Tiny version (not only 0.083+, which verify by default) and needs a CA bundle (Mozilla::CA or the operating system's); 0 connects without checking the server's identity
ua object new HTTP::Tiny Your own user agent: any object with HTTP::Tiny-compatible get/post returning {status, content, headers, success}

Methods

Method Returns Description
lookup($ip, %opts) hashref Full lookup for an IP (omit $ip for caller's IP)
lookup_mine(%opts) hashref Full lookup for the caller's own IP
lookup_field($ip, $field) varies Single field via path API
bulk(\@ips, %opts) arrayref Bulk lookup for up to 100 IPs
asn($ip) hashref ASN data (asn, name, domain, route, type)
threat($ip) hashref Threat flags (is_tor, is_vpn, is_proxy, is_threat, ...), blocklists and scores
carrier($ip) hashref Mobile carrier (name, mcc, mnc)
currency($ip) hashref Currency (name, code, symbol, native, plural)
time_zone($ip) hashref Timezone (name, abbr, offset, is_dst, current_time)
languages($ip) arrayref Languages (name, native, code)
country_name($ip) string Country name
country_code($ip) string ISO 3166-1 alpha-2 country code
is_eu($ip) boolean EU member state
is_satellite($ip) boolean Satellite internet provider
is_anycast($ip) boolean Anycast address

API Response Structure

A full lookup returns a hashref with this shape:

{
    ip             => "8.8.8.8",
    is_eu          => false,
    is_satellite   => true,     # present only when true
    is_anycast     => true,     # present only when true
    city           => "Mountain View",
    region         => "California",
    region_code    => "CA",
    country_name   => "United States",
    country_code   => "US",
    continent_name => "North America",
    continent_code => "NA",
    latitude       => 37.386,
    longitude      => -122.0838,
    postal         => "94035",
    calling_code   => "1",
    flag           => "https://ipdata.co/flags/us.png",
    emoji_flag     => "\x{1f1fa}\x{1f1f8}",
    emoji_unicode  => "U+1F1FA U+1F1F8",

    asn       => { asn, name, domain, route, type },
    company   => { name, domain, network, type },
    carrier   => { name, mcc, mnc },
    languages => [{ name, native, code }],
    currency  => { name, code, symbol, native, plural },
    time_zone => { name, abbr, offset, is_dst, current_time },
    threat    => { is_tor, is_vpn, is_icloud_relay, is_proxy, is_datacenter,
                   is_anonymous, is_known_attacker, is_known_abuser,
                   is_threat, is_bogon,
                   blocklists => [{ name, site, type,
                                    first_seen, last_seen, days_seen }],
                   scores => { vpn_score, proxy_score,
                               threat_score, trust_score } },
}

The JSON is passed through unchanged, so keys the API adds later simply appear in the hashref. Five fields deserve a note:

Field Type Description
is_satellite boolean The address belongs to a satellite internet provider. The API includes the key only when it is true, so test it with a plain if ($result->{is_satellite}), never with exists; a missing key reads as false
is_anycast boolean The address is an anycast address. Present only when true, like is_satellite
threat.blocklists[].first_seen string First calendar date (UTC, YYYY-MM-DD) on which that blocklist listed the address; left as a string
threat.blocklists[].last_seen string Most recent such date (UTC, YYYY-MM-DD); left as a string
threat.blocklists[].days_seen integer Number of days that blocklist has listed the address

Some feeds, for example ipdata's own vpn feed, omit all three blocklist fields, so check for them with exists before use. Bulk results carry the same shape per item.

Requirements

Testing

Run the unit tests (no API key and no network access needed):

make test

Run the full suite including live API tests:

IPDATA_API_KEY=your_key_here make test

Releasing

Releases are made by CI; nothing is tagged by hand. To publish a new version:

  1. In a pull request, bump $VERSION in lib/Net/IPData.pm and give the Unreleased entry in Changes its version number and date.
  2. Merge it. CI tags the merge commit vX.Y.Z, creates the GitHub release with generated notes and uploads the distribution to CPAN.

A merge whose version is already tagged publishes nothing. The release trigger is kept, so publishing a GitHub release by hand still uploads its tag to CPAN.

License

This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

Links

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages