vpndetection (vpndetection v1.0.0)

View Source

The official Erlang client for the VPNDetection API.

Build a client once with new/1, pass the term around, and release it with close/1. Every call answers {ok, Term} or {error, Error}; nothing here raises for a failure the API can report.

Absent is not false. A result is a map whose plan-gated keys are simply MISSING when your plan does not include them, which is a different answer from the key being present and false. Use maps:get(is_hosting, Result, undefined) or maps:find(is_hosting, Result) when the distinction matters, and maps:get(is_hosting, Result, false) when it does not.

Summary

Functions

Release the client's cache. Safe to call on a client that has none.

Every digest published for one dataset file.

Download one dataset file to Path, and answer how many bytes landed.

Download one dataset file and hand back its bytes.

The time-limited URL for one dataset file.

Your organization's recent download attempts, newest first.

The dataset FAMILIES your organization is licensed to download.

What is inside one dataset: schema, samples, row count, sizes.

Whether an address is private, loopback, link-local, documentation, multicast or otherwise not routable, including the IPv6 equivalents and the 6to4 and Teredo ranges.

is_bogon/1, reachable from a client you already hold.

Classify one address.

Classify many addresses at once, one process per address up to the concurrency bound.

Build a client.

Types

batch_options/0

-type batch_options() :: #{retries => non_neg_integer(), concurrency => pos_integer()}.

client/0

-opaque client()

format/0

-type format() :: csvgz | mmdb.

lookup_options/0

-type lookup_options() :: #{retries => non_neg_integer()}.

options/0

-type options() ::
          #{api_key => binary() | string(),
            base_url => binary() | string(),
            cache => #{max => pos_integer(), ttl_ms => pos_integer()} | false,
            concurrency => pos_integer(),
            retries => non_neg_integer(),
            timeout_ms => pos_integer(),
            http => vpndetection_http:http_fun()}.

Functions

close(_)

-spec close(client()) -> ok.

Release the client's cache. Safe to call on a client that has none.

database_checksums(Client, Id, Format)

-spec database_checksums(client(), binary() | string(), format()) ->
                            {ok, map()} | {error, vpndetection_error:error()}.

Every digest published for one dataset file.

The whole set, not one algorithm: which digests a dataset publishes is the API's choice rather than ours, and they arrive nested under checksums.

database_download(Client, Id, Format, Path)

-spec database_download(client(), binary() | string(), format(), binary() | string()) ->
                           {ok, non_neg_integer()} | {error, vpndetection_error:error()}.

Download one dataset file to Path, and answer how many bytes landed.

The transfer is streamed, so nothing beyond a single chunk is ever held in memory whatever the dataset weighs. The bytes go to a neighboring .part file that is renamed only once the whole body has arrived: a transfer that dies halfway leaves neither a truncated file that reads as a complete dataset nor a .part for the next attempt to append to.

The client's timeout_ms bounds the wait between chunks here rather than the whole transfer, because a deadline that suits a lookup is the wrong one for a gigabyte while a stalled transfer is stalled at any size.

database_download_bytes(Client, Id, Format)

-spec database_download_bytes(client(), binary() | string(), format()) ->
                                 {ok, binary()} | {error, vpndetection_error:error()}.

Download one dataset file and hand back its bytes.

This holds the ENTIRE file in memory, and the catalog spans five orders of magnitude, from cdn_ip_v1 at 10 KB to resproxy_ip_90d_v1 at 1.79 GB, so reach for it at the small end and use database_download/4 for anything you have not measured. It transfers over exactly the same streamed path, so the bytes are the ones database_download/4 would have written.

database_download_url(Client, Id, Format)

-spec database_download_url(client(), binary() | string(), format()) ->
                               {ok, binary()} | {error, vpndetection_error:error()}.

The time-limited URL for one dataset file.

The URL is returned rather than the bytes, so the caller decides how to transfer a file that routinely runs to gigabytes. The link authorizes the START of a transfer, so one already running is not interrupted when it lapses.

database_downloads(Client)

-spec database_downloads(client()) -> {ok, [map()]} | {error, vpndetection_error:error()}.

Your organization's recent download attempts, newest first.

database_list(Client)

-spec database_list(client()) -> {ok, [map()]} | {error, vpndetection_error:error()}.

The dataset FAMILIES your organization is licensed to download.

A licence covers a family (vpn_ip), while a download names one of its versions (vpn_ip_v1), so the ids database_download/4 and database_checksums/3 take come from a family's <<"versions">> rather than from the family itself.

Every database response keeps its wire keys as BINARIES. The lookup result is the one place a spec-defined name becomes an atom, because the dataset metadata is keyed by dataset column names, which are the server's to choose and would otherwise fill the atom table.

database_metadata(Client, Id)

-spec database_metadata(client(), binary() | string()) ->
                           {ok, map()} | {error, vpndetection_error:error()}.

What is inside one dataset: schema, samples, row count, sizes.

is_bogon(Ip)

-spec is_bogon(binary() | string()) -> boolean().

Whether an address is private, loopback, link-local, documentation, multicast or otherwise not routable, including the IPv6 equivalents and the 6to4 and Teredo ranges.

These are the addresses lookup/2 answers locally, so they cost no request and no quota. Usable without a client.

is_bogon(Client, Ip)

-spec is_bogon(client(), binary() | string()) -> boolean().

is_bogon/1, reachable from a client you already hold.

lookup(Client, Ip)

-spec lookup(client(), binary() | string()) ->
                {ok, vpndetection_result:result()} | {error, vpndetection_error:error()}.

lookup(Client, Ip, Options)

-spec lookup(client(), binary() | string(), lookup_options()) ->
                {ok, vpndetection_result:result()} | {error, vpndetection_error:error()}.

Classify one address.

A bogon is answered locally and never reaches the network. Everything else is served, then cached for this client alone.

lookup_batch(Client, Ips)

-spec lookup_batch(client(), [binary() | string()]) ->
                      #{binary() =>
                            {ok, vpndetection_result:result()} | {error, vpndetection_error:error()}}.

lookup_batch(Client, Ips, Options)

-spec lookup_batch(client(), [binary() | string()], batch_options()) ->
                      #{binary() =>
                            {ok, vpndetection_result:result()} | {error, vpndetection_error:error()}}.

Classify many addresses at once, one process per address up to the concurrency bound.

Keyed by address rather than positional, so duplicates in the input collapse to a single request and the caller never has to line two lists up. An address that fails carries its {error, Error} as its value, so one bad entry cannot lose the rest of the answers. Erlang maps have no insertion order, so the result is a set of keys rather than a sequence.

concurrency and retries are overridable here, per call, so one large batch does not need a second client built to widen it.

new()

-spec new() -> client().

new(Options)

-spec new(options()) -> client().

Build a client.

api_key is optional: without one you get the free tier, which answers ip and is_vpn and allows 1000 requests per day per source address.

The cache is per client and never shared, because two clients holding different keys are on different plans and so entitled to different fields. Unless cache is false this starts a process linked to the caller, so build clients somewhere long lived and close/1 them when you are done.