# [<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="24"/>](https://vpndetection.io/) VPNDetection Perl Client Library

[![CPAN](https://img.shields.io/cpan/v/VPNDetection.svg)](https://metacpan.org/dist/VPNDetection)
[![license](https://img.shields.io/cpan/l/VPNDetection.svg)](LICENSE)

The official Perl client library for the [VPNDetection](https://vpndetection.io) API.

The library helps you query VPNDetection's APIs for anonymity detection including VPNs, residential proxies, Tor nodes, hosting servers, CDNs, relays and more.

## Getting Started

```bash
cpanm VPNDetection
```

Requires Perl 5.22 or newer. [Mojolicious](https://metacpan.org/dist/Mojolicious) is the only runtime dependency, plus `IO::Socket::SSL` for TLS.

## Usage

**No API key needed to start.** The free tier answers `ip` and `is_vpn`, and allows 1000 requests per day per source address.

```perl
use VPNDetection;

my $client = VPNDetection->new;

my $result = $client->lookup('45.83.91.1');
print $result->is_vpn;   # 1
```

### With an API key

An API key raises your quota, and raises your features on a paid plan. Create one in the [console](https://app.vpndetection.io), then pass it in:

```perl
my $client = VPNDetection->new(api_key => $ENV{VPNDETECTION_API_KEY});

my $result = $client->lookup('45.83.91.1');
print $result->is_vpn;               # 1
print $result->vpn->{provider};      # mullvad
print $result->is_hosting;           # 1
print $result->hosting->{provider};
```

### Batch lookup

You can do batch lookups with a list, which parallelizes requests for you efficiently:

```perl
my $answers = $client->lookup_batch(['45.83.91.1', '8.8.8.8', '1.1.1.1']);

for my $ip (keys %$answers) {
    my $answer = $answers->{$ip};
    if ($answer->isa('VPNDetection::Error')) {
        warn "$ip: $answer";
        next;
    }
    print "$ip: ", $answer->is_vpn, "\n";
}
```

Results are keyed by address, so duplicates in your list collapse into a single request and one address failing never loses the rest. Perl hashes carry no insertion order, so iterate your own list when order matters.

Concurrency and other variables are configurable per-call:

```perl
my $answers = $client->lookup_batch(\@many_ips, concurrency => 32, retries => 4);
```

### Caching

Answers are cached by default, so repeat lookups of the same address are free:

```perl
my $client = VPNDetection->new;

my $result = $client->lookup('45.83.91.1');
print $result->is_vpn;    # 1, API request

my $again = $client->lookup('45.83.91.1');
print $again->is_vpn;     # 1, no API request, result was cached
```

You can change the default cache variables (max size, TTL in seconds) on initialization, or even disable it:

```perl
my $client = VPNDetection->new(cache_size => 50_000, cache_ttl => 6 * 60 * 60);
my $uncached = VPNDetection->new(cache_size => 0);
```

The cache belongs to the client, never to the process. Two clients holding different keys are on different plans and entitled to different fields, so a shared cache would serve one of them the other's shape.

### Private and reserved addresses

Private, loopback, link-local, documentation and multicast addresses (and their IPv6 equivalents, including the 6to4 and Teredo ranges) can never be VPN or proxy infrastructure. The library answers them locally, so they cost no request and no quota:

```perl
my $result = $client->lookup('192.168.1.1');
$result->is_bogon;    # 1, this answer was computed rather than served
$result->is_vpn;      # 0
```

The check is available on the client, which is handy when your inputs are addresses anyway:

```perl
$client->is_bogon('10.0.0.1');    # 1
$client->is_bogon('8.8.8.8');     # 0
```

It is also importable on its own, if you want it without a client:

```perl
use VPNDetection 'is_bogon';

is_bogon('10.0.0.1');    # 1
```

### Errors

Failures die with a `VPNDetection::Error` carrying a `kind` and a `retryable` flag. It stringifies to its message, so it reads like an ordinary string exception where you do not care which it is:

```perl
my $result = eval { $client->lookup('1.1.1.1') };
if (my $err = $@) {
    die $err unless ref $err && $err->isa('VPNDetection::Error');
    warn $err->kind, ' ', $err->retryable;
}
```

`kind` is one of `bad_request`, `unauthorized`, `forbidden`, `rate_limited`, `quota_exceeded`, `server_error` or `network`.

Note that `rate_limited` and `quota_exceeded` both arrive as HTTP 429 and are not the same thing. A rate limit is when the API faces extreme traffic bursts and so retrying later works; but a spent quota needs your allowance raised or the window to roll over. The library retries rate limits for you, but not if your quota is exceeded.

### Non-blocking use

Every call has a `_p` twin returning a [Mojo::Promise](https://metacpan.org/pod/Mojo::Promise), so the library drops into a Mojolicious application without a worker or a thread:

```perl
$client->lookup_p('45.83.91.1')
    ->then(sub { print shift->is_vpn })
    ->catch(sub { warn shift })
    ->wait;
```

The blocking calls are those same promises plus a `wait`, so both paths retry, cache and short-circuit identically. Inside an already running event loop the blocking calls cannot block, and say so rather than returning nothing.

### Database downloads

If your key carries the `db.download` scope, the licensed datasets are available through `$client->database`. A license covers a dataset *family*, and the id you download is the one hanging off its `versions`:

```perl
my $datasets = $client->database->list;
my $id = $datasets->[0]{versions}[0]{id};    # e.g. vpn_ip_v1
```

There are three ways to fetch one: as a link you transfer yourself, as bytes, or straight to a file.

```perl
my $db = $client->database;

my $url = $db->download_url($id, 'mmdb');                  # a time-limited link
my $bytes = $db->download_bytes('cdn_ip_v1', 'csvgz');     # in memory
my $written = $db->download($id, 'mmdb', "./$id.mmdb");    # streamed to disk
```

`download` holds nothing beyond one chunk however large the dataset is, writes through a neighboring `.part` file so a transfer that dies half way leaves nothing that reads as a whole dataset, and raises rather than accepts a body that stops early. `download_bytes` holds the **whole file in memory**, and the catalog runs from `cdn_ip_v1` at 10 KB to `resproxy_ip_90d_v1` at 1.79 GB, so use `download` for anything you have not measured.

### Absent is not false

A field your plan does not include is absent, which never means "we checked and found nothing". Perl makes that easy to miss, since `undef` and `0` are both false.

```perl
if ($result->is_hosting // 0) { ... }     # absent counts as false
if ($result->has('is_hosting')) { ... }   # is this field in my plan?
```

## Other Libraries

There are official VPNDetection client libraries available for many languages including PHP, Python, Go, Java, Ruby, and many popular frameworks such as Django, Rails, and Laravel. See our GitHub at https://github.com/vpndetection-io for more.

## About VPNDetection

VPN Detection API: Accurate anonymity detection identifying VPNs, residential proxies, hosting servers, Tor nodes, CDNs, relays and more.

[<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="96"/>](https://vpndetection.io/)

## License

This project is licensed under the [MIT License](LICENSE).
