vpndetection 5.4.1 → 5.4.2
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 +2 -2
- data/lib/vpndetection/client.rb +13 -4
- data/lib/vpndetection/database_api.rb +2 -1
- data/lib/vpndetection/oauth_api.rb +21 -3
- data/lib/vpndetection/retries.rb +13 -3
- data/lib/vpndetection/transport.rb +19 -2
- data/lib/vpndetection/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ccf5841f50ec64ff7328341110f3ec6cd0affc6260a0588fe22cb746d5d62f1
|
|
4
|
+
data.tar.gz: 99459a01b5d0139a64d039a2259a08c965cd4319c9cae6283dbbc0b113ac5b2c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fbdbee3d38ba2e223be9fc4e1cb53193f71e1891f0e59351d4c59f2eb2a8cc418476f48e0b866fb7c0b6d36fbb166686c46c22049deae80403f8a9a0675ab797
|
|
7
|
+
data.tar.gz: 94d1e3c266372d5b7fe571ea2d370a38dbd91cd11f184f3f06ef7b3f231ddddbc6a261551185a9020453d0704d8911c6c8b21e6de569cb43d4a4cba562d6684f
|
data/README.md
CHANGED
|
@@ -150,7 +150,7 @@ end
|
|
|
150
150
|
|
|
151
151
|
`kind` is one of `:bad_request`, `:unauthorized`, `:forbidden`, `:rate_limited`, `:quota_exceeded`, `:server_error` or `:network`.
|
|
152
152
|
|
|
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.
|
|
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. It waits the `Retry-After` the API sent, or its own backoff from 250 ms when that's past about 24.8 days.
|
|
154
154
|
|
|
155
155
|
### Timeouts and retries
|
|
156
156
|
|
|
@@ -160,7 +160,7 @@ client = VPNDetection::Client.new(timeout: 10, retries: 4)
|
|
|
160
160
|
result = client.lookup('45.83.91.1', timeout: 2, retries: 0)
|
|
161
161
|
```
|
|
162
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 and every `client.database` call that is not a transfer takes `timeout:` (from 5.3.0). A database download bounds only its connection with it, because a whole transfer can take minutes.
|
|
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, and 0 means no bound; 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 and every `client.database` call that is not a transfer takes `timeout:` (from 5.3.0). A database download bounds only its connection with it, because a whole transfer can take minutes. A negative value, anything that isn't a number, and anything past 2147483.647 seconds (the longest curl holds) raise `ArgumentError` where you pass them, before any request.
|
|
164
164
|
|
|
165
165
|
### Database downloads
|
|
166
166
|
|
data/lib/vpndetection/client.rb
CHANGED
|
@@ -29,11 +29,17 @@ module VPNDetection
|
|
|
29
29
|
# @param cache [Boolean] pass false to disable caching.
|
|
30
30
|
# @param cache_ttl [Numeric] how long an answer stays fresh, in seconds.
|
|
31
31
|
# @param concurrency [Integer] batch requests - chunks of up to 1000 addresses - in flight during a batch.
|
|
32
|
-
# @param retries [Integer] extra attempts for a transient failure.
|
|
32
|
+
# @param retries [Integer] extra attempts for a transient failure. Each waits
|
|
33
|
+
# the server's `Retry-After` when it sent one of at most about 24.8 days,
|
|
34
|
+
# and otherwise a backoff of 250 ms that doubles per retry.
|
|
33
35
|
# @param timeout [Numeric] seconds one request may take before it is
|
|
34
|
-
# abandoned. Applies per ATTEMPT, so a retried call may take
|
|
35
|
-
# total, and every call that takes `retries:` also takes
|
|
36
|
-
# override it. A dataset transfer bounds only its connect
|
|
36
|
+
# abandoned, 0 for no bound. Applies per ATTEMPT, so a retried call may take
|
|
37
|
+
# longer in total, and every call that takes `retries:` also takes
|
|
38
|
+
# `timeout:` to override it. A dataset transfer bounds only its connect
|
|
39
|
+
# phase with it.
|
|
40
|
+
# @raise [ArgumentError] for a `timeout`, here or on any call, that is
|
|
41
|
+
# negative, not a finite number, or past 2147483.647 seconds (2**31 - 1 ms),
|
|
42
|
+
# the longest curl holds.
|
|
37
43
|
# @param transport [Transport, nil] override the HTTP layer, mostly for tests.
|
|
38
44
|
def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, cache: true,
|
|
39
45
|
cache_max_size: DEFAULT_CACHE_MAX_SIZE, cache_ttl: DEFAULT_CACHE_TTL,
|
|
@@ -68,6 +74,8 @@ module VPNDetection
|
|
|
68
74
|
# @param retries [Integer, nil] extra attempts, for THIS call only.
|
|
69
75
|
# @param timeout [Numeric, nil] seconds each attempt may take, for THIS call only.
|
|
70
76
|
def lookup(ip, retries: nil, timeout: nil)
|
|
77
|
+
# Here as well as in the transport: a bogon or a cached answer returns before any request.
|
|
78
|
+
Transport.checked_timeout(timeout) unless timeout.nil?
|
|
71
79
|
return Bogon.result(ip) if Bogon.bogon?(ip)
|
|
72
80
|
|
|
73
81
|
hit = @cache&.get(ip)
|
|
@@ -142,6 +150,7 @@ module VPNDetection
|
|
|
142
150
|
unless limit.is_a?(Numeric) && limit >= 1
|
|
143
151
|
raise Error.new(:bad_request, "concurrency must be at least 1, not #{limit.inspect}")
|
|
144
152
|
end
|
|
153
|
+
Transport.checked_timeout(timeout) unless timeout.nil?
|
|
145
154
|
|
|
146
155
|
addresses = ips.to_a.uniq
|
|
147
156
|
answers = {}
|
|
@@ -7,7 +7,8 @@ module VPNDetection
|
|
|
7
7
|
# needs a key carrying the `db.download` scope.
|
|
8
8
|
#
|
|
9
9
|
# Every JSON call here takes `timeout:`, in seconds, bounding each ATTEMPT of
|
|
10
|
-
# that call alone and overriding the bound the client was built with
|
|
10
|
+
# that call alone and overriding the bound the client was built with; one the
|
|
11
|
+
# client would refuse raises ArgumentError before any request. The two
|
|
11
12
|
# transfers take none, and are refused it rather than ignoring it: a dataset
|
|
12
13
|
# runs to gigabytes and minutes, so a bound that suits a JSON call would
|
|
13
14
|
# abandon a healthy download.
|
|
@@ -17,6 +17,8 @@ module VPNDetection
|
|
|
17
17
|
TOKEN_PATH = '/oauth/token'
|
|
18
18
|
REVOKE_PATH = '/oauth/revoke'
|
|
19
19
|
DEVICE_CODE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code'
|
|
20
|
+
# The longest single `sleep` the poll asks Ruby for, in seconds.
|
|
21
|
+
LONGEST_SLEEP = 2**31 - 1
|
|
20
22
|
|
|
21
23
|
# The members a 2xx must carry for its type to mean anything.
|
|
22
24
|
REQUIRED = {
|
|
@@ -29,7 +31,7 @@ module VPNDetection
|
|
|
29
31
|
@transport = transport
|
|
30
32
|
@retries = retries
|
|
31
33
|
# The poll's wait and its monotonic clock, which a test replaces together.
|
|
32
|
-
@wait = ->(seconds) {
|
|
34
|
+
@wait = ->(seconds) { sleep_in_parts(seconds) }
|
|
33
35
|
@now = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
34
36
|
end
|
|
35
37
|
|
|
@@ -96,7 +98,8 @@ module VPNDetection
|
|
|
96
98
|
# Wait for the person to approve a device sign-in, and return its tokens.
|
|
97
99
|
#
|
|
98
100
|
# Waits `device.interval` seconds before EVERY exchange, the first included,
|
|
99
|
-
# and five seconds longer for good each time the server answers `slow_down
|
|
101
|
+
# and five seconds longer for good each time the server answers `slow_down`,
|
|
102
|
+
# but never past `device.expires_in`: a wait that would end later ends then.
|
|
100
103
|
# Raises {OauthAccessDeniedError} when the person refuses and
|
|
101
104
|
# {OauthExpiredTokenError} when the code expires, including locally, with no
|
|
102
105
|
# status, once `device.expires_in` seconds have passed since this call. Any
|
|
@@ -109,10 +112,14 @@ module VPNDetection
|
|
|
109
112
|
# @param timeout [Numeric, nil] bounds each exchange, never the whole wait.
|
|
110
113
|
# @return [TokenResponse]
|
|
111
114
|
def poll_device_token(client_id, device, timeout: nil)
|
|
115
|
+
Transport.checked_timeout(timeout) unless timeout.nil?
|
|
112
116
|
interval = device.interval >= 1 ? device.interval : 5
|
|
113
117
|
deadline = @now.call + device.expires_in
|
|
114
118
|
loop do
|
|
115
|
-
|
|
119
|
+
# A wait that would end past the deadline waits only the time left, never
|
|
120
|
+
# a negative remainder, and the local expiry below follows with no request.
|
|
121
|
+
left = deadline - @now.call
|
|
122
|
+
@wait.call(interval < left ? interval : [left, 0].max)
|
|
116
123
|
raise OauthExpiredTokenError, 'expired_token' if @now.call >= deadline
|
|
117
124
|
|
|
118
125
|
begin
|
|
@@ -129,6 +136,17 @@ module VPNDetection
|
|
|
129
136
|
|
|
130
137
|
private
|
|
131
138
|
|
|
139
|
+
# `sleep` raises RangeError for a Float from about 9.2e18 s and an Integer
|
|
140
|
+
# past 2**63 - 1, and a server's `expires_in` can leave a wait longer than
|
|
141
|
+
# either, so a long one is slept in parts rather than ending the poll.
|
|
142
|
+
def sleep_in_parts(seconds)
|
|
143
|
+
while seconds.positive?
|
|
144
|
+
part = [seconds, LONGEST_SLEEP].min
|
|
145
|
+
sleep(part)
|
|
146
|
+
seconds -= part
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
132
150
|
def exchange(form, timeout)
|
|
133
151
|
decode(TokenResponse, @transport.oauth_request(:POST, TOKEN_PATH, form: form, timeout: timeout).run)
|
|
134
152
|
end
|
data/lib/vpndetection/retries.rb
CHANGED
|
@@ -9,6 +9,10 @@ module VPNDetection
|
|
|
9
9
|
module Retries
|
|
10
10
|
BACKOFF_SECONDS = 0.25
|
|
11
11
|
|
|
12
|
+
# The longest `Retry-After` honored, in seconds: 2**31 - 1 ms, about 24.8
|
|
13
|
+
# days, the same bound as the .NET, erlang, perl and php SDKs'.
|
|
14
|
+
LONGEST_WAIT = 2_147_483.647
|
|
15
|
+
|
|
12
16
|
module_function
|
|
13
17
|
|
|
14
18
|
# `retry_if`, when given, is asked after each retryable failure, and a false
|
|
@@ -26,10 +30,16 @@ module VPNDetection
|
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
|
|
29
|
-
# A server-supplied delay
|
|
30
|
-
#
|
|
33
|
+
# A server-supplied delay wins, including a `Retry-After: 0`, which is the
|
|
34
|
+
# server saying "immediately" rather than saying nothing. One past
|
|
35
|
+
# LONGEST_WAIT is waited out on the backoff instead, still rate_limited:
|
|
36
|
+
# honored, `2147484` held the call for 24.8 days, and `sleep` raised a raw
|
|
37
|
+
# RangeError for `9223372036854775807` and `1e400`.
|
|
31
38
|
def delay_for(error, attempt)
|
|
32
|
-
error.retry_after_seconds
|
|
39
|
+
asked = error.retry_after_seconds
|
|
40
|
+
return asked if asked && asked <= LONGEST_WAIT
|
|
41
|
+
|
|
42
|
+
BACKOFF_SECONDS * (2**(attempt - 1))
|
|
33
43
|
end
|
|
34
44
|
end
|
|
35
45
|
end
|
|
@@ -17,6 +17,23 @@ module VPNDetection
|
|
|
17
17
|
ENTITLEMENT_PATH = '/api/v1/entitlement'
|
|
18
18
|
BATCH_PATH = '/batch'
|
|
19
19
|
|
|
20
|
+
# The longest `timeout` curl holds, in seconds: 2**31 - 1 ms, about 24.8 days.
|
|
21
|
+
# libcurl refuses a longer one, and any negative, and Ethon ignores the
|
|
22
|
+
# refusal, so the call would run with no bound at all.
|
|
23
|
+
LONGEST_TIMEOUT = 2_147_483.647
|
|
24
|
+
|
|
25
|
+
# `timeout`, once it is a bound curl can hold: seconds, 0 for none. Refused
|
|
26
|
+
# where it is set, because nothing downstream refuses it: a negative ran with
|
|
27
|
+
# no bound, and NaN, Infinity, a number past curl's ceiling or a string failed
|
|
28
|
+
# every call with an error from Ruby, FFI or Ethon that names none of this.
|
|
29
|
+
# NaN fails both comparisons, and an infinity one of them.
|
|
30
|
+
def self.checked_timeout(timeout)
|
|
31
|
+
return timeout if timeout.is_a?(Numeric) && timeout.real? && timeout >= 0 && timeout <= LONGEST_TIMEOUT
|
|
32
|
+
|
|
33
|
+
raise ArgumentError,
|
|
34
|
+
"timeout must be a number of seconds from 0 (no bound) to #{LONGEST_TIMEOUT}, not #{timeout.inspect}"
|
|
35
|
+
end
|
|
36
|
+
|
|
20
37
|
# The generated Configuration applies EVERY security scheme the spec lists,
|
|
21
38
|
# so a keyless client would send `Authorization: Bearer `, an empty
|
|
22
39
|
# `X-Api-Key` and an empty `apikey` query parameter. The API answers 401 to
|
|
@@ -29,7 +46,7 @@ module VPNDetection
|
|
|
29
46
|
self.host = uri.port == uri.default_port ? uri.host : "#{uri.host}:#{uri.port}"
|
|
30
47
|
self.base_path = uri.path
|
|
31
48
|
self.access_token = api_key
|
|
32
|
-
self.timeout = timeout
|
|
49
|
+
self.timeout = timeout.nil? ? nil : Transport.checked_timeout(timeout)
|
|
33
50
|
end
|
|
34
51
|
|
|
35
52
|
def auth_settings
|
|
@@ -59,7 +76,7 @@ module VPNDetection
|
|
|
59
76
|
def build_request(http_method, path, opts = {})
|
|
60
77
|
request = super
|
|
61
78
|
request.options[:followlocation] = false
|
|
62
|
-
request.options[:timeout] = opts[:timeout] unless opts[:timeout].nil?
|
|
79
|
+
request.options[:timeout] = Transport.checked_timeout(opts[:timeout]) unless opts[:timeout].nil?
|
|
63
80
|
request
|
|
64
81
|
end
|
|
65
82
|
|
data/lib/vpndetection/version.rb
CHANGED