vpndetection 5.0.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 +31 -4
- 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 +69 -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 +77 -30
- data/lib/vpndetection/configuration.rb +1 -1
- data/lib/vpndetection/database_api.rb +18 -10
- data/lib/vpndetection/errors.rb +61 -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 +193 -0
- data/lib/vpndetection/models/batch_lookup_request.rb +185 -0
- data/lib/vpndetection/models/batch_lookup_response.rb +196 -0
- 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 +80 -4
- data/lib/vpndetection/version.rb +1 -1
- data/lib/vpndetection.rb +1 -0
- metadata +5 -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
|
-
|
|
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'])
|
|
@@ -84,12 +84,12 @@ results.each do |ip, result|
|
|
|
84
84
|
end
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
Results are keyed by address, so duplicates in your list collapse into a single
|
|
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
|
-
|
|
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:
|
|
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
|
|
@@ -19,6 +19,74 @@ module VPNDetection
|
|
|
19
19
|
def initialize(api_client = ApiClient.default)
|
|
20
20
|
@api_client = api_client
|
|
21
21
|
end
|
|
22
|
+
# Batch
|
|
23
|
+
# Answers up to 1000 addresses in one call. Each distinct string in `ips` is one lookup: it costs exactly what `GET /{ip}` costs for that address and comes back with exactly the fields that call would carry for your plan. Exact duplicates collapse to one entry and one lookup. Both maps in the answer are keyed by the string you sent, so nothing has to be lined up by position; the `ip` inside each result is the normalized form. An address that could not be answered sits in `errors` with the status and message the single lookup would have given, and never disturbs the others: a string that is not an address is a `400` there, and an allowance that runs out part way through leaves the remaining entries as `429`s. The call itself fails only for the reasons below, and a `429` on the call always carries `Retry-After`: the batch is admitted or refused whole by the rate limit, so a per-entry `429` is always a spent allowance and never a throttle.
|
|
24
|
+
# @param batch_lookup_request [BatchLookupRequest]
|
|
25
|
+
# @param [Hash] opts the optional parameters
|
|
26
|
+
# @return [BatchLookupResponse]
|
|
27
|
+
def lookup_batch(batch_lookup_request, opts = {})
|
|
28
|
+
data, _status_code, _headers = lookup_batch_with_http_info(batch_lookup_request, opts)
|
|
29
|
+
data
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Batch
|
|
33
|
+
# Answers up to 1000 addresses in one call. Each distinct string in `ips` is one lookup: it costs exactly what `GET /{ip}` costs for that address and comes back with exactly the fields that call would carry for your plan. Exact duplicates collapse to one entry and one lookup. Both maps in the answer are keyed by the string you sent, so nothing has to be lined up by position; the `ip` inside each result is the normalized form. An address that could not be answered sits in `errors` with the status and message the single lookup would have given, and never disturbs the others: a string that is not an address is a `400` there, and an allowance that runs out part way through leaves the remaining entries as `429`s. The call itself fails only for the reasons below, and a `429` on the call always carries `Retry-After`: the batch is admitted or refused whole by the rate limit, so a per-entry `429` is always a spent allowance and never a throttle.
|
|
34
|
+
# @param batch_lookup_request [BatchLookupRequest]
|
|
35
|
+
# @param [Hash] opts the optional parameters
|
|
36
|
+
# @return [Array<(BatchLookupResponse, Integer, Hash)>] BatchLookupResponse data, response status code and response headers
|
|
37
|
+
def lookup_batch_with_http_info(batch_lookup_request, opts = {})
|
|
38
|
+
if @api_client.config.debugging
|
|
39
|
+
@api_client.config.logger.debug 'Calling API: LookupWireApi.lookup_batch ...'
|
|
40
|
+
end
|
|
41
|
+
# verify the required parameter 'batch_lookup_request' is set
|
|
42
|
+
if @api_client.config.client_side_validation && batch_lookup_request.nil?
|
|
43
|
+
fail ArgumentError, "Missing the required parameter 'batch_lookup_request' when calling LookupWireApi.lookup_batch"
|
|
44
|
+
end
|
|
45
|
+
# resource path
|
|
46
|
+
local_var_path = '/batch'
|
|
47
|
+
|
|
48
|
+
# query parameters
|
|
49
|
+
query_params = opts[:query_params] || {}
|
|
50
|
+
|
|
51
|
+
# header parameters
|
|
52
|
+
header_params = opts[:header_params] || {}
|
|
53
|
+
# HTTP header 'Accept' (if needed)
|
|
54
|
+
header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
|
|
55
|
+
# HTTP header 'Content-Type'
|
|
56
|
+
content_type = @api_client.select_header_content_type(['application/json'])
|
|
57
|
+
if !content_type.nil?
|
|
58
|
+
header_params['Content-Type'] = content_type
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# form parameters
|
|
62
|
+
form_params = opts[:form_params] || {}
|
|
63
|
+
|
|
64
|
+
# http body (model)
|
|
65
|
+
post_body = opts[:debug_body] || @api_client.object_to_http_body(batch_lookup_request)
|
|
66
|
+
|
|
67
|
+
# return_type
|
|
68
|
+
return_type = opts[:debug_return_type] || 'BatchLookupResponse'
|
|
69
|
+
|
|
70
|
+
# auth_names
|
|
71
|
+
auth_names = opts[:debug_auth_names] || ['apiKeyQuery', 'bearerAuth', 'apiKeyHeader']
|
|
72
|
+
|
|
73
|
+
new_options = opts.merge(
|
|
74
|
+
:operation => :"LookupWireApi.lookup_batch",
|
|
75
|
+
:header_params => header_params,
|
|
76
|
+
:query_params => query_params,
|
|
77
|
+
:form_params => form_params,
|
|
78
|
+
:body => post_body,
|
|
79
|
+
:auth_names => auth_names,
|
|
80
|
+
:return_type => return_type
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
|
|
84
|
+
if @api_client.config.debugging
|
|
85
|
+
@api_client.config.logger.debug "API called: LookupWireApi#lookup_batch\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
|
|
86
|
+
end
|
|
87
|
+
return data, status_code, headers
|
|
88
|
+
end
|
|
89
|
+
|
|
22
90
|
# Lookup
|
|
23
91
|
# Answers what is known about a single IPv4 or IPv6 address. Which fields come back is decided by the plan behind the presented key; with no key the answer is `ip` and `is_vpn`.
|
|
24
92
|
# @param ip [String] The IPv4 or IPv6 address to classify.
|
|
@@ -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,10 @@ 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
|
+
# The most addresses POST /batch takes in one call; a larger batch is sent in
|
|
13
|
+
# chunks of this size.
|
|
14
|
+
BATCH_MAX = 1000
|
|
12
15
|
|
|
13
16
|
# A client for the VPNDetection API.
|
|
14
17
|
#
|
|
@@ -17,14 +20,20 @@ module VPNDetection
|
|
|
17
20
|
class Client
|
|
18
21
|
# The licensed dataset downloads, for keys that carry the `db.download` scope.
|
|
19
22
|
attr_reader :database
|
|
23
|
+
# Signing a person in with OAuth, which needs no API key at all.
|
|
24
|
+
attr_reader :oauth
|
|
20
25
|
|
|
21
26
|
# @param api_key [String, nil] omit it entirely to use the free tier, which
|
|
22
27
|
# answers `ip` and `is_vpn` and allows 1000 requests per day per source
|
|
23
28
|
# address.
|
|
24
29
|
# @param cache [Boolean] pass false to disable caching.
|
|
25
30
|
# @param cache_ttl [Numeric] how long an answer stays fresh, in seconds.
|
|
26
|
-
# @param concurrency [Integer] in
|
|
31
|
+
# @param concurrency [Integer] batch requests - chunks of up to 1000 addresses - in flight during a batch.
|
|
27
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.
|
|
28
37
|
# @param transport [Transport, nil] override the HTTP layer, mostly for tests.
|
|
29
38
|
def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, cache: true,
|
|
30
39
|
cache_max_size: DEFAULT_CACHE_MAX_SIZE, cache_ttl: DEFAULT_CACHE_TTL,
|
|
@@ -37,6 +46,7 @@ module VPNDetection
|
|
|
37
46
|
@concurrency = concurrency
|
|
38
47
|
@retries = retries
|
|
39
48
|
@database = DatabaseApi.new(@transport, retries: retries)
|
|
49
|
+
@oauth = OauthApi.new(@transport, retries: retries)
|
|
40
50
|
end
|
|
41
51
|
|
|
42
52
|
# Whether an address is private, loopback, link-local, documentation,
|
|
@@ -54,14 +64,17 @@ module VPNDetection
|
|
|
54
64
|
#
|
|
55
65
|
# A bogon is answered locally and never reaches the network. Everything else
|
|
56
66
|
# is served, then cached for this instance.
|
|
57
|
-
|
|
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)
|
|
58
71
|
return Bogon.result(ip) if Bogon.bogon?(ip)
|
|
59
72
|
|
|
60
73
|
hit = @cache&.get(ip)
|
|
61
74
|
return hit unless hit.nil?
|
|
62
75
|
|
|
63
76
|
result = Retries.with_retries(retries || @retries) do
|
|
64
|
-
Transport.lookup_result(@transport.lookup_request(ip).run)
|
|
77
|
+
Transport.lookup_result(@transport.lookup_request(ip, timeout: timeout).run)
|
|
65
78
|
end
|
|
66
79
|
@cache&.set(ip, result)
|
|
67
80
|
result
|
|
@@ -77,9 +90,9 @@ module VPNDetection
|
|
|
77
90
|
# Deliberately NOT cached. The cache is keyed by address, and which address
|
|
78
91
|
# this is IS the question: a machine that moves between networks would
|
|
79
92
|
# otherwise be told where it used to be.
|
|
80
|
-
def my_ip(retries: nil)
|
|
93
|
+
def my_ip(retries: nil, timeout: nil)
|
|
81
94
|
Retries.with_retries(retries || @retries) do
|
|
82
|
-
Transport.lookup_result(@transport.myip_request.run)
|
|
95
|
+
Transport.lookup_result(@transport.myip_request(timeout: timeout).run)
|
|
83
96
|
end
|
|
84
97
|
end
|
|
85
98
|
|
|
@@ -102,21 +115,34 @@ module VPNDetection
|
|
|
102
115
|
# cached answer is a wrong one within seconds of the next request.
|
|
103
116
|
#
|
|
104
117
|
# @return [Entitlement]
|
|
105
|
-
def my_entitlement(retries: nil)
|
|
118
|
+
def my_entitlement(retries: nil, timeout: nil)
|
|
106
119
|
Retries.with_retries(retries || @retries) do
|
|
107
|
-
Transport.entitlement_result(@transport.entitlement_request.run)
|
|
120
|
+
Transport.entitlement_result(@transport.entitlement_request(timeout: timeout).run)
|
|
108
121
|
end
|
|
109
122
|
end
|
|
110
123
|
|
|
111
|
-
# Classify many addresses in
|
|
124
|
+
# Classify many addresses in as few requests as possible.
|
|
112
125
|
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
126
|
+
# Bogons are answered locally and cached answers are reused; everything else
|
|
127
|
+
# goes to the batch endpoint in chunks of up to 1000 addresses, with at most
|
|
128
|
+
# `concurrency` chunks in flight. Keyed by address rather than positional, so
|
|
129
|
+
# duplicates in the input collapse to a single entry and the caller never has
|
|
130
|
+
# to line two lists up. An address that fails carries its error as its value,
|
|
131
|
+
# so one bad entry cannot lose the rest of the answers: the API reports a
|
|
132
|
+
# per-entry failure with the status the single lookup would have answered,
|
|
133
|
+
# and a chunk that fails as a whole marks every address in it.
|
|
117
134
|
#
|
|
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.
|
|
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.
|
|
118
139
|
# @return [Hash{String => Result, Error}] in the order the addresses were given
|
|
119
|
-
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
|
+
|
|
120
146
|
addresses = ips.to_a.uniq
|
|
121
147
|
answers = {}
|
|
122
148
|
pending = []
|
|
@@ -126,7 +152,7 @@ module VPNDetection
|
|
|
126
152
|
hit.nil? ? pending << ip : answers[ip] = hit
|
|
127
153
|
end
|
|
128
154
|
unless pending.empty?
|
|
129
|
-
run_batch(pending, answers,
|
|
155
|
+
run_batch(pending.each_slice(BATCH_MAX).to_a, answers, limit, retries || @retries, timeout)
|
|
130
156
|
end
|
|
131
157
|
|
|
132
158
|
# Reinstated in input order: a hydra settles in completion order, and a
|
|
@@ -138,37 +164,58 @@ module VPNDetection
|
|
|
138
164
|
|
|
139
165
|
# One hydra per call, sized for THIS call. Reusing an instance-level hydra
|
|
140
166
|
# would silently cap a per-call concurrency at the client's setting, and
|
|
141
|
-
# would not be safe to drive from two threads either.
|
|
142
|
-
|
|
167
|
+
# would not be safe to drive from two threads either. Each request is one
|
|
168
|
+
# chunk of up to 1000 addresses.
|
|
169
|
+
def run_batch(chunks, answers, concurrency, retries, timeout)
|
|
143
170
|
hydra = Typhoeus::Hydra.new(max_concurrency: concurrency)
|
|
144
171
|
attempts = Hash.new(0)
|
|
145
172
|
|
|
146
|
-
enqueue = lambda do |
|
|
147
|
-
request = @transport.
|
|
173
|
+
enqueue = lambda do |chunk|
|
|
174
|
+
request = @transport.batch_request(chunk, timeout: timeout)
|
|
148
175
|
request.on_complete do |response|
|
|
149
|
-
outcome = settle(
|
|
150
|
-
answers
|
|
176
|
+
outcome = settle(chunk, response, attempts, retries, enqueue)
|
|
177
|
+
answers.merge!(outcome) unless outcome.nil?
|
|
151
178
|
end
|
|
152
179
|
hydra.queue(request)
|
|
153
180
|
end
|
|
154
181
|
|
|
155
|
-
|
|
182
|
+
chunks.each { |chunk| enqueue.call(chunk) }
|
|
156
183
|
hydra.run
|
|
157
184
|
end
|
|
158
185
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
186
|
+
# One POST /batch, mapped back onto the addresses it was asked about. A
|
|
187
|
+
# chunk-level failure - the call refused, the transport failing, the retries
|
|
188
|
+
# exhausted - becomes every address's error, exactly as it would have been
|
|
189
|
+
# had each been looked up alone.
|
|
190
|
+
def settle(chunk, response, attempts, retries, enqueue)
|
|
191
|
+
body = Transport.batch_body(response)
|
|
192
|
+
chunk.to_h do |ip|
|
|
193
|
+
answer = batch_answer(ip, body)
|
|
194
|
+
@cache&.set(ip, answer) if answer.is_a?(Result)
|
|
195
|
+
[ip, answer]
|
|
196
|
+
end
|
|
163
197
|
rescue Error => e
|
|
164
|
-
return e unless e.retryable? && attempts[
|
|
198
|
+
return chunk.to_h { |ip| [ip, e] } unless e.retryable? && attempts[chunk] < retries
|
|
165
199
|
|
|
166
|
-
attempts[
|
|
200
|
+
attempts[chunk] += 1
|
|
167
201
|
# Sleeping here stalls the whole hydra, which is what a server-supplied
|
|
168
202
|
# delay asks for: it is telling every request to this host to back off.
|
|
169
|
-
sleep(Retries.delay_for(e, attempts[
|
|
170
|
-
enqueue.call(
|
|
203
|
+
sleep(Retries.delay_for(e, attempts[chunk]))
|
|
204
|
+
enqueue.call(chunk)
|
|
171
205
|
nil
|
|
172
206
|
end
|
|
207
|
+
|
|
208
|
+
# Every address lands in exactly one of `results` and `errors`; an address in
|
|
209
|
+
# neither is the server breaking its own contract, and is reported as such
|
|
210
|
+
# rather than lost.
|
|
211
|
+
def batch_answer(ip, body)
|
|
212
|
+
if (served = body['results'][ip])
|
|
213
|
+
Result.new(served)
|
|
214
|
+
elsif (failed = body['errors'][ip])
|
|
215
|
+
Error.from_entry(failed['status'], failed['error'])
|
|
216
|
+
else
|
|
217
|
+
Error.new(:server_error, "the batch answer did not include #{ip}", status: 200)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
173
220
|
end
|
|
174
221
|
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
|
|
@@ -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
|
#
|