vpndetection 5.1.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +30 -3
- data/lib/vpndetection/api/account_wire_api.rb +1 -1
- data/lib/vpndetection/api/api_keys_wire_api.rb +1 -1
- data/lib/vpndetection/api/authorization_wire_api.rb +19 -17
- data/lib/vpndetection/api/database_wire_api.rb +7 -3
- data/lib/vpndetection/api/entitlement_wire_api.rb +1 -1
- data/lib/vpndetection/api/lookup_wire_api.rb +1 -1
- data/lib/vpndetection/api/organization_wire_api.rb +1 -1
- data/lib/vpndetection/api_client.rb +1 -1
- data/lib/vpndetection/api_error.rb +1 -1
- data/lib/vpndetection/api_model_base.rb +1 -1
- data/lib/vpndetection/client.rb +29 -13
- data/lib/vpndetection/configuration.rb +1 -1
- data/lib/vpndetection/database_api.rb +18 -10
- data/lib/vpndetection/errors.rb +54 -13
- data/lib/vpndetection/models/account_create_apikey_request.rb +1 -1
- data/lib/vpndetection/models/account_created_apikey.rb +1 -1
- data/lib/vpndetection/models/account_org.rb +1 -1
- data/lib/vpndetection/models/account_org_ref.rb +1 -1
- data/lib/vpndetection/models/account_org_wrap.rb +1 -1
- data/lib/vpndetection/models/account_rc.rb +1 -1
- data/lib/vpndetection/models/account_revealed_apikey.rb +1 -1
- data/lib/vpndetection/models/account_user.rb +1 -1
- data/lib/vpndetection/models/apikey_detail.rb +1 -1
- data/lib/vpndetection/models/apikey_list.rb +1 -1
- data/lib/vpndetection/models/batch_lookup_error.rb +1 -1
- data/lib/vpndetection/models/batch_lookup_request.rb +1 -1
- data/lib/vpndetection/models/batch_lookup_response.rb +1 -1
- data/lib/vpndetection/models/class_detail.rb +1 -1
- data/lib/vpndetection/models/database.rb +1 -1
- data/lib/vpndetection/models/database_checksums_response.rb +1 -1
- data/lib/vpndetection/models/database_format.rb +1 -1
- data/lib/vpndetection/models/database_format_size.rb +1 -1
- data/lib/vpndetection/models/database_list.rb +1 -1
- data/lib/vpndetection/models/database_metadata.rb +1 -1
- data/lib/vpndetection/models/database_metadata_column.rb +1 -1
- data/lib/vpndetection/models/database_version.rb +1 -1
- data/lib/vpndetection/models/db_checksums.rb +1 -1
- data/lib/vpndetection/models/device_authorization.rb +2 -1
- data/lib/vpndetection/models/download.rb +1 -1
- data/lib/vpndetection/models/download_list.rb +1 -1
- data/lib/vpndetection/models/entitlement.rb +1 -1
- data/lib/vpndetection/models/entitlement_apikey.rb +1 -1
- data/lib/vpndetection/models/entitlement_error.rb +1 -1
- data/lib/vpndetection/models/entitlement_plan.rb +1 -1
- data/lib/vpndetection/models/entitlement_usage.rb +1 -1
- data/lib/vpndetection/models/error_envelope.rb +1 -1
- data/lib/vpndetection/models/identity.rb +1 -1
- data/lib/vpndetection/models/lookup_error.rb +1 -1
- data/lib/vpndetection/models/lookup_response.rb +1 -1
- data/lib/vpndetection/models/oauth_error.rb +1 -1
- data/lib/vpndetection/models/oauth_metadata.rb +30 -9
- data/lib/vpndetection/models/proxy_detail.rb +1 -1
- data/lib/vpndetection/models/standing.rb +1 -1
- data/lib/vpndetection/models/token_response.rb +28 -5
- data/lib/vpndetection/models/vpn_detail.rb +1 -1
- data/lib/vpndetection/oauth_api.rb +166 -0
- data/lib/vpndetection/retries.rb +4 -2
- data/lib/vpndetection/transport.rb +58 -5
- data/lib/vpndetection/version.rb +1 -1
- data/lib/vpndetection.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 199c0adec648e1556593cb36414f58482fdf2f638f59fad25ca04ee43c0de02c
|
|
4
|
+
data.tar.gz: f8a7d728594094a46cfcac9ba2306ebd351bc3d80b785eabd1743252a7f6588e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b148e272ab33acca1a14b5523c448411d92882a6069ba183522e223077ed5185854feeca378608d68009abaf08ee9a1bcffada932cb42e006455992d07e98eb7
|
|
7
|
+
data.tar.gz: a42507f48287500895380ded3951be855dcfd24311933efc9a50e79c3c81a230ab32a9f9378d7de9aa33a9778f4e15c5f5d55e94c0f8f8f60bae519cc51f6ece
|
data/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Usage counts against the anniversary of your subscription, not the calendar mont
|
|
|
70
70
|
|
|
71
71
|
### Batch lookup
|
|
72
72
|
|
|
73
|
-
Look up many addresses at once. Bogons and cached answers are handled locally, and everything else goes to the batch endpoint in chunks of up to 1000 addresses, in parallel:
|
|
73
|
+
Look up as many addresses as you like at once. Bogons and cached answers are handled locally, and everything else goes to the batch endpoint in chunks of up to 1000 addresses, in parallel:
|
|
74
74
|
|
|
75
75
|
```ruby
|
|
76
76
|
results = client.lookup_batch(['45.83.91.1', '8.8.8.8', '1.1.1.1'])
|
|
@@ -86,10 +86,10 @@ end
|
|
|
86
86
|
|
|
87
87
|
Results are keyed by address, in the order you first listed each one, so duplicates in your list collapse into a single entry and one address failing never loses the rest: it carries its error as its value, with the status the API would have given that address on its own.
|
|
88
88
|
|
|
89
|
-
How many chunks are in flight at once,
|
|
89
|
+
How many chunks are in flight at once, how many times a failed chunk is retried, and how long each chunk's request may take are configurable per call:
|
|
90
90
|
|
|
91
91
|
```ruby
|
|
92
|
-
results = client.lookup_batch(many_ips, concurrency: 4, retries: 4)
|
|
92
|
+
results = client.lookup_batch(many_ips, concurrency: 4, retries: 4, timeout: 5)
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
### Caching
|
|
@@ -152,6 +152,16 @@ end
|
|
|
152
152
|
|
|
153
153
|
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.
|
|
154
154
|
|
|
155
|
+
### Timeouts and retries
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
client = VPNDetection::Client.new(timeout: 10, retries: 4)
|
|
159
|
+
|
|
160
|
+
result = client.lookup('45.83.91.1', timeout: 2, retries: 0)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`timeout` is in seconds and bounds each attempt, body included, so a call that is retried can take longer in total. It defaults to 30 seconds; before 5.2.0 the default was 10, so pass `timeout: 10` to keep that bound. The client's values are defaults: `lookup`, `lookup_batch`, `my_ip` and `my_entitlement` each take `timeout:` and `retries:` for that call alone, and every `client.oauth` method takes `timeout:`. A database download bounds only its connection with it, because a whole transfer can take minutes.
|
|
164
|
+
|
|
155
165
|
### Database downloads
|
|
156
166
|
|
|
157
167
|
If your key carries the `db.download` scope, the licensed databases are available through `client.database`. A license covers a database FAMILY and a download names one of its versions, so the id comes from `versions`:
|
|
@@ -167,6 +177,23 @@ bytes = client.database.download_bytes('cdn_ip_v1', 'csvgz')
|
|
|
167
177
|
|
|
168
178
|
`download` streams straight to disk, so nothing bigger than a chunk is ever held in memory whatever the database weighs, and it writes through a neighboring `.part` file so a transfer that dies half way leaves no truncated copy behind. `download_url` hands back the time-limited link and follows nothing, for when you want to run the transfer yourself. `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 reach for `download` for anything you have not measured.
|
|
169
179
|
|
|
180
|
+
### Sign in with OAuth (device flow)
|
|
181
|
+
|
|
182
|
+
A program running on a person's own machine can let them sign in with their browser and pick one of their API keys, instead of asking them to paste one.
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
client = VPNDetection::Client.new
|
|
186
|
+
device = client.oauth.device_authorization('your-client-id', scope: 'account.read apikeys.read apikeys.reveal')
|
|
187
|
+
puts "Open #{device.verification_uri} and enter #{device.user_code}"
|
|
188
|
+
|
|
189
|
+
token = client.oauth.poll_device_token('your-client-id', device)
|
|
190
|
+
raise 'no API key was picked' if token.apikey.nil?
|
|
191
|
+
|
|
192
|
+
keyed = VPNDetection::Client.new(api_key: token.apikey)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`poll_device_token` raises `VPNDetection::OauthAccessDeniedError` when the person refuses and `VPNDetection::OauthExpiredTokenError` when the code expires first. Client IDs are issued on request from support@vpndetection.io, and `client.oauth.revoke('your-client-id', token.refresh_token)` signs the machine out.
|
|
196
|
+
|
|
170
197
|
### Absent is not false
|
|
171
198
|
|
|
172
199
|
Every field beyond `ip` and `is_vpn` is present when your plan includes it and `nil` when it does not. `nil` means "not in your plan"; `false` means "we checked, and no".
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -13,10 +13,12 @@ Generator version: 7.25.0
|
|
|
13
13
|
require 'cgi'
|
|
14
14
|
|
|
15
15
|
module VPNDetection
|
|
16
|
+
# @deprecated Since 5.2.0, and removed in the next major. Use {VPNDetection::Client#oauth}.
|
|
16
17
|
class AuthorizationWireApi
|
|
17
18
|
attr_accessor :api_client
|
|
18
19
|
|
|
19
20
|
def initialize(api_client = ApiClient.default)
|
|
21
|
+
warn("#{self.class} is deprecated; use VPNDetection::Client#oauth", category: :deprecated)
|
|
20
22
|
@api_client = api_client
|
|
21
23
|
end
|
|
22
24
|
# Authorize
|
|
@@ -257,9 +259,9 @@ module VPNDetection
|
|
|
257
259
|
|
|
258
260
|
# Revoke
|
|
259
261
|
# RFC 7009. Always answers 200, including for a token that was never valid - an endpoint that distinguished the two would be a way to test whether a stolen string is a live credential. Revoking a REFRESH token ends the whole authorization and takes its access tokens with it. Revoking an access token affects only that token.
|
|
260
|
-
# @param token [String]
|
|
262
|
+
# @param token [String] An access token or a refresh token.
|
|
261
263
|
# @param [Hash] opts the optional parameters
|
|
262
|
-
# @option opts [String] :client_id
|
|
264
|
+
# @option opts [String] :client_id Accepted and not checked.
|
|
263
265
|
# @return [Object]
|
|
264
266
|
def oauth_revoke(token, opts = {})
|
|
265
267
|
data, _status_code, _headers = oauth_revoke_with_http_info(token, opts)
|
|
@@ -268,9 +270,9 @@ module VPNDetection
|
|
|
268
270
|
|
|
269
271
|
# Revoke
|
|
270
272
|
# RFC 7009. Always answers 200, including for a token that was never valid - an endpoint that distinguished the two would be a way to test whether a stolen string is a live credential. Revoking a REFRESH token ends the whole authorization and takes its access tokens with it. Revoking an access token affects only that token.
|
|
271
|
-
# @param token [String]
|
|
273
|
+
# @param token [String] An access token or a refresh token.
|
|
272
274
|
# @param [Hash] opts the optional parameters
|
|
273
|
-
# @option opts [String] :client_id
|
|
275
|
+
# @option opts [String] :client_id Accepted and not checked.
|
|
274
276
|
# @return [Array<(Object, Integer, Hash)>] Object data, response status code and response headers
|
|
275
277
|
def oauth_revoke_with_http_info(token, opts = {})
|
|
276
278
|
if @api_client.config.debugging
|
|
@@ -329,14 +331,14 @@ module VPNDetection
|
|
|
329
331
|
|
|
330
332
|
# Token
|
|
331
333
|
# Three grant types. `urn:ietf:params:oauth:grant-type:device_code` polls a device authorization. Until the person approves it answers `authorization_pending`; poll faster than `interval` and it answers `slow_down`, which means widen your interval and keep it widened. `authorization_code` exchanges a code from `/oauth/authorize`, with the `code_verifier` matching the challenge you sent. `refresh_token` exchanges a refresh token. The presented token is consumed whatever happens next, so store the new one before using it.
|
|
332
|
-
# @param grant_type [String]
|
|
334
|
+
# @param grant_type [String] `urn:ietf:params:oauth:grant-type:device_code`, `authorization_code` or `refresh_token`.
|
|
333
335
|
# @param client_id [String]
|
|
334
336
|
# @param [Hash] opts the optional parameters
|
|
335
|
-
# @option opts [String] :device_code
|
|
336
|
-
# @option opts [String] :code
|
|
337
|
-
# @option opts [String] :code_verifier
|
|
338
|
-
# @option opts [String] :redirect_uri
|
|
339
|
-
# @option opts [String] :refresh_token
|
|
337
|
+
# @option opts [String] :device_code Required by the device code grant.
|
|
338
|
+
# @option opts [String] :code Required by the authorization code grant.
|
|
339
|
+
# @option opts [String] :code_verifier Required by the authorization code grant.
|
|
340
|
+
# @option opts [String] :redirect_uri Authorization code grant: the `redirect_uri` the code was issued against, exactly.
|
|
341
|
+
# @option opts [String] :refresh_token Required by the refresh token grant.
|
|
340
342
|
# @return [TokenResponse]
|
|
341
343
|
def oauth_token(grant_type, client_id, opts = {})
|
|
342
344
|
data, _status_code, _headers = oauth_token_with_http_info(grant_type, client_id, opts)
|
|
@@ -345,14 +347,14 @@ module VPNDetection
|
|
|
345
347
|
|
|
346
348
|
# Token
|
|
347
349
|
# Three grant types. `urn:ietf:params:oauth:grant-type:device_code` polls a device authorization. Until the person approves it answers `authorization_pending`; poll faster than `interval` and it answers `slow_down`, which means widen your interval and keep it widened. `authorization_code` exchanges a code from `/oauth/authorize`, with the `code_verifier` matching the challenge you sent. `refresh_token` exchanges a refresh token. The presented token is consumed whatever happens next, so store the new one before using it.
|
|
348
|
-
# @param grant_type [String]
|
|
350
|
+
# @param grant_type [String] `urn:ietf:params:oauth:grant-type:device_code`, `authorization_code` or `refresh_token`.
|
|
349
351
|
# @param client_id [String]
|
|
350
352
|
# @param [Hash] opts the optional parameters
|
|
351
|
-
# @option opts [String] :device_code
|
|
352
|
-
# @option opts [String] :code
|
|
353
|
-
# @option opts [String] :code_verifier
|
|
354
|
-
# @option opts [String] :redirect_uri
|
|
355
|
-
# @option opts [String] :refresh_token
|
|
353
|
+
# @option opts [String] :device_code Required by the device code grant.
|
|
354
|
+
# @option opts [String] :code Required by the authorization code grant.
|
|
355
|
+
# @option opts [String] :code_verifier Required by the authorization code grant.
|
|
356
|
+
# @option opts [String] :redirect_uri Authorization code grant: the `redirect_uri` the code was issued against, exactly.
|
|
357
|
+
# @option opts [String] :refresh_token Required by the refresh token grant.
|
|
356
358
|
# @return [Array<(TokenResponse, Integer, Hash)>] TokenResponse data, response status code and response headers
|
|
357
359
|
def oauth_token_with_http_info(grant_type, client_id, opts = {})
|
|
358
360
|
if @api_client.config.debugging
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -285,7 +285,7 @@ module VPNDetection
|
|
|
285
285
|
# History
|
|
286
286
|
# Recent download attempts for this organization, newest first. Refusals are listed too, so a failed transfer can be accounted for.
|
|
287
287
|
# @param [Hash] opts the optional parameters
|
|
288
|
-
# @option opts [Integer] :limit
|
|
288
|
+
# @option opts [Integer] :limit How many attempts to return. Clamped to 1 through 200. (default to 50)
|
|
289
289
|
# @return [DownloadList]
|
|
290
290
|
def list_downloads(opts = {})
|
|
291
291
|
data, _status_code, _headers = list_downloads_with_http_info(opts)
|
|
@@ -295,7 +295,7 @@ module VPNDetection
|
|
|
295
295
|
# History
|
|
296
296
|
# Recent download attempts for this organization, newest first. Refusals are listed too, so a failed transfer can be accounted for.
|
|
297
297
|
# @param [Hash] opts the optional parameters
|
|
298
|
-
# @option opts [Integer] :limit
|
|
298
|
+
# @option opts [Integer] :limit How many attempts to return. Clamped to 1 through 200. (default to 50)
|
|
299
299
|
# @return [Array<(DownloadList, Integer, Hash)>] DownloadList data, response status code and response headers
|
|
300
300
|
def list_downloads_with_http_info(opts = {})
|
|
301
301
|
if @api_client.config.debugging
|
|
@@ -305,6 +305,10 @@ module VPNDetection
|
|
|
305
305
|
fail ArgumentError, 'invalid value for "opts[:"limit"]" when calling DatabaseWireApi.list_downloads, must be smaller than or equal to 200.'
|
|
306
306
|
end
|
|
307
307
|
|
|
308
|
+
if @api_client.config.client_side_validation && !opts[:'limit'].nil? && opts[:'limit'] < 1
|
|
309
|
+
fail ArgumentError, 'invalid value for "opts[:"limit"]" when calling DatabaseWireApi.list_downloads, must be greater than or equal to 1.'
|
|
310
|
+
end
|
|
311
|
+
|
|
308
312
|
# resource path
|
|
309
313
|
local_var_path = '/api/v1/database/downloads'
|
|
310
314
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
data/lib/vpndetection/client.rb
CHANGED
|
@@ -8,7 +8,7 @@ module VPNDetection
|
|
|
8
8
|
DEFAULT_CACHE_TTL = 3600
|
|
9
9
|
DEFAULT_CONCURRENCY = 8
|
|
10
10
|
DEFAULT_RETRIES = 2
|
|
11
|
-
DEFAULT_TIMEOUT =
|
|
11
|
+
DEFAULT_TIMEOUT = 30
|
|
12
12
|
# The most addresses POST /batch takes in one call; a larger batch is sent in
|
|
13
13
|
# chunks of this size.
|
|
14
14
|
BATCH_MAX = 1000
|
|
@@ -20,6 +20,8 @@ module VPNDetection
|
|
|
20
20
|
class Client
|
|
21
21
|
# The licensed dataset downloads, for keys that carry the `db.download` scope.
|
|
22
22
|
attr_reader :database
|
|
23
|
+
# Signing a person in with OAuth, which needs no API key at all.
|
|
24
|
+
attr_reader :oauth
|
|
23
25
|
|
|
24
26
|
# @param api_key [String, nil] omit it entirely to use the free tier, which
|
|
25
27
|
# answers `ip` and `is_vpn` and allows 1000 requests per day per source
|
|
@@ -28,6 +30,10 @@ module VPNDetection
|
|
|
28
30
|
# @param cache_ttl [Numeric] how long an answer stays fresh, in seconds.
|
|
29
31
|
# @param concurrency [Integer] batch requests - chunks of up to 1000 addresses - in flight during a batch.
|
|
30
32
|
# @param retries [Integer] extra attempts for a transient failure.
|
|
33
|
+
# @param timeout [Numeric] seconds one request may take before it is
|
|
34
|
+
# abandoned. Applies per ATTEMPT, so a retried call may take longer in
|
|
35
|
+
# total, and every call that takes `retries:` also takes `timeout:` to
|
|
36
|
+
# override it. A dataset transfer bounds only its connect phase with it.
|
|
31
37
|
# @param transport [Transport, nil] override the HTTP layer, mostly for tests.
|
|
32
38
|
def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, cache: true,
|
|
33
39
|
cache_max_size: DEFAULT_CACHE_MAX_SIZE, cache_ttl: DEFAULT_CACHE_TTL,
|
|
@@ -40,6 +46,7 @@ module VPNDetection
|
|
|
40
46
|
@concurrency = concurrency
|
|
41
47
|
@retries = retries
|
|
42
48
|
@database = DatabaseApi.new(@transport, retries: retries)
|
|
49
|
+
@oauth = OauthApi.new(@transport, retries: retries)
|
|
43
50
|
end
|
|
44
51
|
|
|
45
52
|
# Whether an address is private, loopback, link-local, documentation,
|
|
@@ -57,14 +64,17 @@ module VPNDetection
|
|
|
57
64
|
#
|
|
58
65
|
# A bogon is answered locally and never reaches the network. Everything else
|
|
59
66
|
# is served, then cached for this instance.
|
|
60
|
-
|
|
67
|
+
#
|
|
68
|
+
# @param retries [Integer, nil] extra attempts, for THIS call only.
|
|
69
|
+
# @param timeout [Numeric, nil] seconds each attempt may take, for THIS call only.
|
|
70
|
+
def lookup(ip, retries: nil, timeout: nil)
|
|
61
71
|
return Bogon.result(ip) if Bogon.bogon?(ip)
|
|
62
72
|
|
|
63
73
|
hit = @cache&.get(ip)
|
|
64
74
|
return hit unless hit.nil?
|
|
65
75
|
|
|
66
76
|
result = Retries.with_retries(retries || @retries) do
|
|
67
|
-
Transport.lookup_result(@transport.lookup_request(ip).run)
|
|
77
|
+
Transport.lookup_result(@transport.lookup_request(ip, timeout: timeout).run)
|
|
68
78
|
end
|
|
69
79
|
@cache&.set(ip, result)
|
|
70
80
|
result
|
|
@@ -80,9 +90,9 @@ module VPNDetection
|
|
|
80
90
|
# Deliberately NOT cached. The cache is keyed by address, and which address
|
|
81
91
|
# this is IS the question: a machine that moves between networks would
|
|
82
92
|
# otherwise be told where it used to be.
|
|
83
|
-
def my_ip(retries: nil)
|
|
93
|
+
def my_ip(retries: nil, timeout: nil)
|
|
84
94
|
Retries.with_retries(retries || @retries) do
|
|
85
|
-
Transport.lookup_result(@transport.myip_request.run)
|
|
95
|
+
Transport.lookup_result(@transport.myip_request(timeout: timeout).run)
|
|
86
96
|
end
|
|
87
97
|
end
|
|
88
98
|
|
|
@@ -105,9 +115,9 @@ module VPNDetection
|
|
|
105
115
|
# cached answer is a wrong one within seconds of the next request.
|
|
106
116
|
#
|
|
107
117
|
# @return [Entitlement]
|
|
108
|
-
def my_entitlement(retries: nil)
|
|
118
|
+
def my_entitlement(retries: nil, timeout: nil)
|
|
109
119
|
Retries.with_retries(retries || @retries) do
|
|
110
|
-
Transport.entitlement_result(@transport.entitlement_request.run)
|
|
120
|
+
Transport.entitlement_result(@transport.entitlement_request(timeout: timeout).run)
|
|
111
121
|
end
|
|
112
122
|
end
|
|
113
123
|
|
|
@@ -122,10 +132,17 @@ module VPNDetection
|
|
|
122
132
|
# per-entry failure with the status the single lookup would have answered,
|
|
123
133
|
# and a chunk that fails as a whole marks every address in it.
|
|
124
134
|
#
|
|
125
|
-
# @param concurrency [Integer, nil] chunks in flight, for THIS batch only.
|
|
135
|
+
# @param concurrency [Integer, nil] chunks in flight, for THIS batch only. Below 1
|
|
136
|
+
# is refused as `:bad_request` before any request, since nothing could ever run.
|
|
126
137
|
# @param retries [Integer, nil] extra attempts for a failed chunk, for THIS batch only.
|
|
138
|
+
# @param timeout [Numeric, nil] seconds each chunk's attempt may take, for THIS batch only.
|
|
127
139
|
# @return [Hash{String => Result, Error}] in the order the addresses were given
|
|
128
|
-
def lookup_batch(ips, concurrency: nil, retries: nil)
|
|
140
|
+
def lookup_batch(ips, concurrency: nil, retries: nil, timeout: nil)
|
|
141
|
+
limit = concurrency || @concurrency
|
|
142
|
+
unless limit.is_a?(Numeric) && limit >= 1
|
|
143
|
+
raise Error.new(:bad_request, "concurrency must be at least 1, not #{limit.inspect}")
|
|
144
|
+
end
|
|
145
|
+
|
|
129
146
|
addresses = ips.to_a.uniq
|
|
130
147
|
answers = {}
|
|
131
148
|
pending = []
|
|
@@ -135,8 +152,7 @@ module VPNDetection
|
|
|
135
152
|
hit.nil? ? pending << ip : answers[ip] = hit
|
|
136
153
|
end
|
|
137
154
|
unless pending.empty?
|
|
138
|
-
run_batch(pending.each_slice(BATCH_MAX).to_a, answers,
|
|
139
|
-
concurrency || @concurrency, retries || @retries)
|
|
155
|
+
run_batch(pending.each_slice(BATCH_MAX).to_a, answers, limit, retries || @retries, timeout)
|
|
140
156
|
end
|
|
141
157
|
|
|
142
158
|
# Reinstated in input order: a hydra settles in completion order, and a
|
|
@@ -150,12 +166,12 @@ module VPNDetection
|
|
|
150
166
|
# would silently cap a per-call concurrency at the client's setting, and
|
|
151
167
|
# would not be safe to drive from two threads either. Each request is one
|
|
152
168
|
# chunk of up to 1000 addresses.
|
|
153
|
-
def run_batch(chunks, answers, concurrency, retries)
|
|
169
|
+
def run_batch(chunks, answers, concurrency, retries, timeout)
|
|
154
170
|
hydra = Typhoeus::Hydra.new(max_concurrency: concurrency)
|
|
155
171
|
attempts = Hash.new(0)
|
|
156
172
|
|
|
157
173
|
enqueue = lambda do |chunk|
|
|
158
|
-
request = @transport.batch_request(chunk)
|
|
174
|
+
request = @transport.batch_request(chunk, timeout: timeout)
|
|
159
175
|
request.on_complete do |response|
|
|
160
176
|
outcome = settle(chunk, response, attempts, retries, enqueue)
|
|
161
177
|
answers.merge!(outcome) unless outcome.nil?
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -63,11 +63,7 @@ module VPNDetection
|
|
|
63
63
|
partial = "#{path}.part"
|
|
64
64
|
begin
|
|
65
65
|
url = download_url(id, format)
|
|
66
|
-
written =
|
|
67
|
-
# Reopened per attempt, so a retry restarts the file rather than
|
|
68
|
-
# appending a second copy of the body to a half-written one.
|
|
69
|
-
File.open(partial, 'wb') { |file| stream(url) { |chunk| file.write(chunk) } }
|
|
70
|
-
end
|
|
66
|
+
written = File.open(partial, 'wb') { |file| transfer(url) { |chunk| file.write(chunk) } }
|
|
71
67
|
File.rename(partial, path)
|
|
72
68
|
rescue StandardError
|
|
73
69
|
File.delete(partial) if File.exist?(partial)
|
|
@@ -84,11 +80,9 @@ module VPNDetection
|
|
|
84
80
|
# and use {#download} for anything you have not measured.
|
|
85
81
|
def download_bytes(id, format)
|
|
86
82
|
url = download_url(id, format)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
bytes
|
|
91
|
-
end
|
|
83
|
+
bytes = String.new(encoding: Encoding::BINARY)
|
|
84
|
+
transfer(url) { |chunk| bytes << chunk }
|
|
85
|
+
bytes
|
|
92
86
|
end
|
|
93
87
|
|
|
94
88
|
private
|
|
@@ -106,6 +100,20 @@ module VPNDetection
|
|
|
106
100
|
"invalid value for \"format\", must be one of #{DatabaseFormat.all_vars}"
|
|
107
101
|
end
|
|
108
102
|
|
|
103
|
+
# The transfer of a presigned link, retried only while nothing has reached the
|
|
104
|
+
# block: object storage failing before the body is as transient as any
|
|
105
|
+
# outage, while a body that dies part way is not fetched again, because the
|
|
106
|
+
# bytes already handed over cannot be taken back.
|
|
107
|
+
def transfer(url, &sink)
|
|
108
|
+
delivered = false
|
|
109
|
+
Retries.with_retries(@retries, retry_if: -> { !delivered }) do
|
|
110
|
+
stream(url) do |chunk|
|
|
111
|
+
delivered = true
|
|
112
|
+
sink.call(chunk)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
109
117
|
# Runs one transfer of a presigned link, handing each chunk to the block, and
|
|
110
118
|
# returns the bytes that reached it.
|
|
111
119
|
#
|
data/lib/vpndetection/errors.rb
CHANGED
|
@@ -38,25 +38,26 @@ module VPNDetection
|
|
|
38
38
|
def self.from_status(status, headers, body, message: nil)
|
|
39
39
|
message ||= message_of(body) || "request failed with status #{status}"
|
|
40
40
|
retry_after = parse_retry_after(header(headers, 'retry-after'))
|
|
41
|
+
kind = kind_for(status, headers)
|
|
41
42
|
|
|
43
|
+
new(kind, message, status: status, retry_after_seconds: kind == :rate_limited ? retry_after : nil)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# What a response with this status is, whatever its body says.
|
|
47
|
+
def self.kind_for(status, headers)
|
|
42
48
|
case status
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
new(:rate_limited, message, status: status, retry_after_seconds: retry_after)
|
|
50
|
-
end
|
|
51
|
-
when 400 then new(:bad_request, message, status: status)
|
|
52
|
-
when 401 then new(:unauthorized, message, status: status)
|
|
53
|
-
when 403 then new(:forbidden, message, status: status)
|
|
49
|
+
# Present means transient, absent means an allowance is spent. Nothing
|
|
50
|
+
# else in the response separates the two.
|
|
51
|
+
when 429 then parse_retry_after(header(headers, 'retry-after')).nil? ? :quota_exceeded : :rate_limited
|
|
52
|
+
when 400 then :bad_request
|
|
53
|
+
when 401 then :unauthorized
|
|
54
|
+
when 403 then :forbidden
|
|
54
55
|
# Every other 4xx is a CLIENT error. Classifying on the RANGE rather than
|
|
55
56
|
# on an enumerated list is what keeps a 404 from a bad dataset id falling
|
|
56
57
|
# through to the retryable server_error default and being retried twice
|
|
57
58
|
# before it fails.
|
|
58
|
-
when 400..499 then
|
|
59
|
-
else
|
|
59
|
+
when 400..499 then :bad_request
|
|
60
|
+
else :server_error
|
|
60
61
|
end
|
|
61
62
|
end
|
|
62
63
|
|
|
@@ -111,4 +112,44 @@ module VPNDetection
|
|
|
111
112
|
|
|
112
113
|
private_class_method :header, :message_of, :parse_retry_after
|
|
113
114
|
end
|
|
115
|
+
|
|
116
|
+
# The authorization server refusing an OAuth request: a 4xx whose body names an
|
|
117
|
+
# RFC 6749 `error` code. Never retryable, whatever the status, because every
|
|
118
|
+
# code it can carry answers the request as it was made.
|
|
119
|
+
#
|
|
120
|
+
# Not `OauthError`: that constant is the generated model of the error body.
|
|
121
|
+
class OauthRequestError < Error
|
|
122
|
+
# The `error` code, such as `slow_down` or `invalid_grant`.
|
|
123
|
+
attr_reader :error_code
|
|
124
|
+
# `error_description` when the server sent one as a string, otherwise nil.
|
|
125
|
+
attr_reader :error_description
|
|
126
|
+
|
|
127
|
+
# The refusal a response carries, as the most specific class its code has.
|
|
128
|
+
def self.for_code(error_code, error_description, status:, headers: {})
|
|
129
|
+
klass = case error_code
|
|
130
|
+
when 'access_denied' then OauthAccessDeniedError
|
|
131
|
+
when 'expired_token' then OauthExpiredTokenError
|
|
132
|
+
else OauthRequestError
|
|
133
|
+
end
|
|
134
|
+
klass.new(error_code, error_description, kind: Error.kind_for(status, headers), status: status)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def initialize(error_code, error_description = nil, kind: :bad_request, status: nil)
|
|
138
|
+
super(kind, error_description.nil? ? error_code : "#{error_code}: #{error_description}", status: status)
|
|
139
|
+
@error_code = error_code
|
|
140
|
+
@error_description = error_description
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def retryable?
|
|
144
|
+
false
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The person refused the sign-in. Their device code is spent.
|
|
149
|
+
class OauthAccessDeniedError < OauthRequestError; end
|
|
150
|
+
|
|
151
|
+
# The device code is no longer valid: it expired, or was already exchanged or
|
|
152
|
+
# refused. Raised with no status when {OauthApi#poll_device_token} reaches the
|
|
153
|
+
# code's lifetime before the server says so.
|
|
154
|
+
class OauthExpiredTokenError < OauthRequestError; end
|
|
114
155
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
#The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
|
|
5
5
|
|
|
6
|
-
The version of the OpenAPI document: 2026.09.
|
|
6
|
+
The version of the OpenAPI document: 2026.09.16
|
|
7
7
|
Contact: support@vpndetection.io
|
|
8
8
|
Generated by: https://openapi-generator.tech
|
|
9
9
|
Generator version: 7.25.0
|