vpndetection (vpndetection v3.3.2)

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.

Every format a dataset file is published in: the values format() takes, for checking one that came from a flag or a config file.

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.

Every value a family's <<"license_type">> takes when it is not null.

Classify one address.

Classify many addresses in as few requests as possible.

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.

Start a device sign-in: show the person user_code and verification_uri, then call oauth_poll_device_token/3.

Exchange a device code for tokens, once. Until the person approves, this answers {error, #{error_code := <<"authorization_pending">>}}; oauth_poll_device_token/3 is the loop that waits for them.

Exchange a refresh token for a new pair. The token presented is spent, so keep the refresh_token this answers. Never retried; the answer may name the key in apikey_id but never carries apikey.

The authorization server's discovery document.

Wait for the person to approve a device sign-in, and answer its tokens.

Revoke an access or a refresh token. A refresh token ends the whole grant and every token it issued, which is how a machine signs out.

Every value a family's &lt;&lt;"standing"&gt;&gt; takes in database_list/1.

Types

batch_options/0

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

client/0

-opaque client()

device_authorization_options/0

-type device_authorization_options() ::
          #{scope => binary() | string(),
            resource => binary() | string(),
            timeout_ms => pos_integer() | infinity}.

downloads_options/0

-type downloads_options() :: #{limit => pos_integer(), timeout_ms => pos_integer() | infinity}.

format/0

-type format() :: csvgz | mmdb.

The same set at runtime, because format() checks nothing once compiled.

lookup_options/0

-type lookup_options() :: #{retries => non_neg_integer(), timeout_ms => pos_integer() | infinity}.

oauth_options/0

-type oauth_options() :: #{timeout_ms => pos_integer() | infinity}.

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() | infinity,
            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()}.

database_downloads(Client, Options)

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

Your organization's recent download attempts, newest first.

Refusals are listed too: a denial is what answers "it stopped working", and its absence answers nothing. limit defaults to 50 and the API clamps it to 200. timeout_ms bounds each attempt of this call alone, so a retried call can take longer in total.

database_formats()

-spec database_formats() -> [format()].

Every format a dataset file is published in: the values format() takes, for checking one that came from a flag or a config file.

database_list(Client)

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

The dataset FAMILIES your organization is licensed to download.

A license 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 &lt;&lt;"versions"&gt;&gt; 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.

license_types()

-spec license_types() -> [binary()].

Every value a family's &lt;&lt;"license_type"&gt;&gt; takes when it is not null.

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.

Options overrides the client's retries and timeout_ms for this call alone. timeout_ms bounds each attempt, so a retried call can take longer in total.

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 in as few requests as possible.

Bogons are answered locally and cached answers are reused; everything else goes to the batch endpoint in chunks of up to 1000 addresses, with at most concurrency chunks in flight. Keyed by address rather than positional, so duplicates in the input collapse to a single entry 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: the API reports a per-entry failure with the status the single lookup would have answered, and a chunk that fails as a whole marks every address in it. Erlang maps have no insertion order, so the result is a set of keys rather than a sequence.

concurrency, retries and timeout_ms are overridable here, per call, so one large batch does not need a second client built to widen it. There is no cap on how many addresses one call takes; chunking them is this function's job.

my_entitlement(Client)

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

my_entitlement(Client, Options)

-spec my_entitlement(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 what the key you are calling WITH may spend.

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.

my_ip(Client)

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

my_ip(Client, Options)

-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.

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.

oauth_device_authorization(Client, ClientId)

-spec oauth_device_authorization(client(), binary() | string()) ->
                                    {ok, vpndetection_oauth:device_authorization()} |
                                    {error, vpndetection_error:error()}.

oauth_device_authorization(Client, ClientId, Options)

-spec oauth_device_authorization(client(), binary() | string(), device_authorization_options()) ->
                                    {ok, vpndetection_oauth:device_authorization()} |
                                    {error, vpndetection_error:error()}.

Start a device sign-in: show the person user_code and verification_uri, then call oauth_poll_device_token/3.

Client IDs are issued on request from support@vpndetection.io. scope is one space-delimited string, sent as given; the server grants what the client may ask for and silently drops the rest.

oauth_exchange_device_code(Client, ClientId, DeviceCode)

-spec oauth_exchange_device_code(client(), binary() | string(), binary() | string()) ->
                                    {ok, vpndetection_oauth:token_response()} |
                                    {error, vpndetection_error:error()}.

oauth_exchange_device_code(Client, ClientId, DeviceCode, Options)

-spec oauth_exchange_device_code(client(), binary() | string(), binary() | string(), oauth_options()) ->
                                    {ok, vpndetection_oauth:token_response()} |
                                    {error, vpndetection_error:error()}.

Exchange a device code for tokens, once. Until the person approves, this answers {error, #{error_code := <<"authorization_pending">>}}; oauth_poll_device_token/3 is the loop that waits for them.

Never retried: the server spends the code when it answers, so a retry after a lost success could only fail and lose the tokens. The answer carries apikey_id and apikey when the person picked a key.

oauth_exchange_refresh_token(Client, ClientId, RefreshToken)

-spec oauth_exchange_refresh_token(client(), binary() | string(), binary() | string()) ->
                                      {ok, vpndetection_oauth:token_response()} |
                                      {error, vpndetection_error:error()}.

oauth_exchange_refresh_token(Client, ClientId, RefreshToken, Options)

-spec oauth_exchange_refresh_token(client(), binary() | string(), binary() | string(), oauth_options()) ->
                                      {ok, vpndetection_oauth:token_response()} |
                                      {error, vpndetection_error:error()}.

Exchange a refresh token for a new pair. The token presented is spent, so keep the refresh_token this answers. Never retried; the answer may name the key in apikey_id but never carries apikey.

oauth_metadata(Client)

-spec oauth_metadata(client()) ->
                        {ok, vpndetection_oauth:metadata()} | {error, vpndetection_error:error()}.

oauth_metadata(Client, Options)

-spec oauth_metadata(client(), oauth_options()) ->
                        {ok, vpndetection_oauth:metadata()} | {error, vpndetection_error:error()}.

The authorization server's discovery document.

Every oauth_* call sends NO credential, whatever the client was built with, and works the same on a client built without a key. timeout_ms in Options bounds each attempt of this call alone.

oauth_poll_device_token(Client, ClientId, Device)

oauth_poll_device_token(Client, ClientId, Device, Options)

Wait for the person to approve a device sign-in, and answer its tokens.

Waits the device's interval seconds before EVERY exchange, the first included, and five seconds longer for good each time the server answers slow_down. A refusal answers error_code<<"access_denied">>; an expired code <<"expired_token">>, with no status when the device's expires_in ran out here first. Any other failure, a timeout or an outage included, ends the wait unchanged; calling again with the same device is safe until it expires.

It blocks the calling process until one of those outcomes. There is no cancellation handle, so run it in a process you can kill. timeout_ms bounds each exchange, never the whole wait.

oauth_revoke(Client, ClientId, Token)

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

oauth_revoke(Client, ClientId, Token, Options)

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

Revoke an access or a refresh token. A refresh token ends the whole grant and every token it issued, which is how a machine signs out.

standings()

-spec standings() -> [binary()].

Every value a family's &lt;&lt;"standing"&gt;&gt; takes in database_list/1.