vpndetection (vpndetection v2.1.0)
View SourceThe 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.
What this client's key is entitled to, and how much of it has been used.
Classify the address this client is calling from.
Build a client.
Types
-type batch_options() :: #{retries => non_neg_integer(), concurrency => pos_integer()}.
-opaque client()
-type format() :: csvgz | mmdb.
-type lookup_options() :: #{retries => non_neg_integer()}.
-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
-spec close(client()) -> ok.
Release the client's cache. Safe to call on a client that has none.
-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.
-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.
-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.
-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.
-spec database_downloads(client()) -> {ok, [map()]} | {error, vpndetection_error:error()}.
Your organization's recent download attempts, newest first.
-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.
-spec database_metadata(client(), binary() | string()) -> {ok, map()} | {error, vpndetection_error:error()}.
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.
These are the addresses lookup/2 answers locally, so they cost no request and no quota. Usable without a client.
is_bogon/1, reachable from a client you already hold.
-spec lookup(client(), binary() | string()) -> {ok, vpndetection_result:result()} | {error, vpndetection_error:error()}.
-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.
-spec lookup_batch(client(), [binary() | string()]) -> #{binary() => {ok, vpndetection_result:result()} | {error, vpndetection_error:error()}}.
-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.
-spec my_account(client()) -> {ok, map()} | {error, vpndetection_error:error()}.
-spec my_account(client(), lookup_options()) -> {ok, map()} | {error, vpndetection_error:error()}.
What this client's key is entitled to, and how much of it has been used.
Named for what it answers rather than me, which sits one letter from my_ip/1 and means something quite different: one is which address you are calling FROM, the other is which account you are calling AS.
Unlike a lookup there is no useful unauthenticated answer, so a client built without an API key gets an unauthorized error rather than a partial one.
Usage counts against the ALLOWANCE WINDOW - the anniversary of the subscription, not the calendar month and not the billing period - and it is the same number a lookup is gated on. hard_limit is null when we never stop serving, which is not the same as a limit of zero.
Deliberately NOT cached: the whole point is what has been spent, and a cached answer is a wrong one within seconds of the next request.
-spec my_ip(client()) -> {ok, vpndetection_result:result()} | {error, vpndetection_error:error()}.
-spec my_ip(client(), lookup_options()) -> {ok, vpndetection_result:result()} | {error, vpndetection_error:error()}.
Classify the address this client is calling from.
The same answer lookup/2 would give for that address, at the same cost against your allowance. The address is the one our edge observed, so a call made through a proxy or a VPN reports the exit it left through - usually the point of asking.
Deliberately NOT cached. The cache is keyed by address, and which address this is IS the question: a machine that moves between networks would otherwise be told where it used to be.
-spec new() -> 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.