ipwhois 1.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 +7 -0
- data/CHANGELOG.md +37 -0
- data/LICENSE +21 -0
- data/README.md +351 -0
- data/lib/ipwhois/client.rb +380 -0
- data/lib/ipwhois/version.rb +6 -0
- data/lib/ipwhois.rb +30 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 439b7008b4082c53b21db0360b7d1b1914e890d069e43cbd451fae937a6768d6
|
|
4
|
+
data.tar.gz: f410bbc7ba36f0f3f0cecb0744ec1bbc3be19e98737bf064acee1b6be6f69395
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6fc2bb733df8f1a9fdee24b6cdb63e03aa6881e0a92b54ee33b894ed96a5ff547222ef8c5b30c12da39247df4226e21aba33a11a989f2ee54ce2d56680c6c529
|
|
7
|
+
data.tar.gz: 4188276ad8f840c73ef56b8a4623fc8fbb845b2f20af7961fb7ad2232a5457e047c20318c98baf5f0321352ba1157cccd22c5a35cc477149527893ce95624c13
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `ipwhois` gem will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.2.0] - 2026-05-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial public release of the `ipwhois` gem.
|
|
13
|
+
- `Ipwhois::Client#lookup` — single IP lookup (IPv4 / IPv6, or current IP).
|
|
14
|
+
- `Ipwhois::Client#bulk_lookup` — up to 100 IPs in a single GET request
|
|
15
|
+
(paid plan).
|
|
16
|
+
- Free plan (no API key, `ipwho.is`) and paid plan (with API key,
|
|
17
|
+
`ipwhois.pro`) served by the same `Client` class.
|
|
18
|
+
- HTTPS by default; `ssl: false` constructor option to fall back to HTTP.
|
|
19
|
+
- Localisation (`:lang`), field filtering (`:fields`), threat detection
|
|
20
|
+
(`:security`), rate-limit info (`:rate`).
|
|
21
|
+
- Fluent client-wide defaults: `set_language`, `set_fields`, `set_security`,
|
|
22
|
+
`set_rate`, `set_timeout`, `set_connect_timeout`, `set_user_agent` — each
|
|
23
|
+
returns `self` and is chainable.
|
|
24
|
+
- **The library never raises.** All errors — invalid input, network failure,
|
|
25
|
+
API-level errors (bad IP, bad key, rate limit, …) — are returned in the
|
|
26
|
+
response hash with `'success' => false` and a `'message'`. HTTP error
|
|
27
|
+
responses are additionally enriched with `'http_status'`, and HTTP 429
|
|
28
|
+
responses on the free plan with `'retry_after'`.
|
|
29
|
+
- Every error response carries an `'error_type'` field: one of `'api'`,
|
|
30
|
+
`'network'`, or `'invalid_argument'`, so callers can branch on the failure
|
|
31
|
+
category with a single `info['error_type']` check.
|
|
32
|
+
- Minitest test suite covering URL construction, IPv6 path encoding, and
|
|
33
|
+
input validation. No real HTTP request is sent — the suite runs anywhere
|
|
34
|
+
without an API key or network access.
|
|
35
|
+
- No runtime dependencies — uses only the Ruby stdlib (`net/http`, `uri`,
|
|
36
|
+
`json`, `openssl`).
|
|
37
|
+
- Ruby `>= 3.0` requirement.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ipwhois.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# ipwhois
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/ipwhois)
|
|
4
|
+
[](https://www.ruby-lang.org)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Official, dependency-free Ruby client for the [ipwhois.io](https://ipwhois.io)
|
|
8
|
+
IP Geolocation API.
|
|
9
|
+
|
|
10
|
+
- ✅ Single and bulk IP lookups (IPv4 and IPv6)
|
|
11
|
+
- ✅ Works with both the **Free** and **Paid** plans
|
|
12
|
+
- ✅ HTTPS by default
|
|
13
|
+
- ✅ Localisation, field selection, threat detection, rate info
|
|
14
|
+
- ✅ Never raises — all errors returned as `'success' => false` hashes
|
|
15
|
+
- ✅ No runtime dependencies — Ruby stdlib only (`net/http`, `uri`, `json`, `openssl`)
|
|
16
|
+
- ✅ Ruby 3.0+
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Add to your `Gemfile`:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem 'ipwhois'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then run:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bundle install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or install directly:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
gem install ipwhois
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Free vs Paid plan
|
|
39
|
+
|
|
40
|
+
The same `Ipwhois::Client` class is used for both plans. The only difference is
|
|
41
|
+
whether you pass an API key:
|
|
42
|
+
|
|
43
|
+
- **Free plan** — create the client **without arguments**. No API key, no
|
|
44
|
+
signup required. Suitable for low-traffic and non-commercial use.
|
|
45
|
+
- **Paid plan** — create the client **with your API key** from
|
|
46
|
+
<https://ipwhois.io>. Higher limits, plus access to bulk lookups and
|
|
47
|
+
threat-detection data.
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
free = Ipwhois::Client.new # Free plan — no API key
|
|
51
|
+
paid = Ipwhois::Client.new('YOUR_API_KEY') # Paid plan — with API key
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Everything else (`lookup`, options, error handling) is identical.
|
|
55
|
+
|
|
56
|
+
## Quick start — Free plan (no API key)
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
require 'ipwhois'
|
|
60
|
+
|
|
61
|
+
client = Ipwhois::Client.new # no API key
|
|
62
|
+
|
|
63
|
+
info = client.lookup('8.8.8.8')
|
|
64
|
+
|
|
65
|
+
puts "#{info['country']} #{info.dig('flag', 'emoji')}"
|
|
66
|
+
# → United States 🇺🇸
|
|
67
|
+
|
|
68
|
+
puts "#{info['city']}, #{info['region']}"
|
|
69
|
+
# → Mountain View, California
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick start — Paid plan (with API key)
|
|
73
|
+
|
|
74
|
+
Get an API key at <https://ipwhois.io> and pass it to the constructor:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
require 'ipwhois'
|
|
78
|
+
|
|
79
|
+
client = Ipwhois::Client.new('YOUR_API_KEY') # with API key
|
|
80
|
+
|
|
81
|
+
info = client.lookup('8.8.8.8')
|
|
82
|
+
|
|
83
|
+
puts "#{info['country']} #{info.dig('flag', 'emoji')}"
|
|
84
|
+
# → United States 🇺🇸
|
|
85
|
+
|
|
86
|
+
puts "#{info['city']}, #{info['region']}"
|
|
87
|
+
# → Mountain View, California
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> ℹ️ Pass nothing to look up your own public IP: `client.lookup` — works
|
|
91
|
+
> on both plans.
|
|
92
|
+
|
|
93
|
+
## Lookup options
|
|
94
|
+
|
|
95
|
+
Every option below can be passed per call, or set once on the client as a
|
|
96
|
+
default.
|
|
97
|
+
|
|
98
|
+
| Option | Type | Plans needed | Description |
|
|
99
|
+
| ------------------ | -------- | -------------------- | -------------------------------------------------------------------------- |
|
|
100
|
+
| `:lang` | String | Free + Paid | One of: `'en'`, `'ru'`, `'de'`, `'es'`, `'pt-BR'`, `'fr'`, `'zh-CN'`, `'ja'` |
|
|
101
|
+
| `:fields` | Array | Free + Paid | Restrict the response to specific fields (e.g. `%w[country city]`) |
|
|
102
|
+
| `:rate` | Boolean | Basic and above | Include the `rate` block (`limit`, `remaining`) |
|
|
103
|
+
| `:security` | Boolean | Business and above | Include the `security` block (proxy/vpn/tor/hosting) |
|
|
104
|
+
|
|
105
|
+
### Setting defaults once
|
|
106
|
+
|
|
107
|
+
Every option can be passed two ways: **per call** (as keyword arguments to
|
|
108
|
+
`lookup` / `bulk_lookup`) or **once as a default** on the client. Per-call
|
|
109
|
+
options always override the defaults, so it's safe to set sensible defaults
|
|
110
|
+
and only override what differs for a specific call.
|
|
111
|
+
|
|
112
|
+
Defaults are set with fluent setters — `set_language`, `set_fields`,
|
|
113
|
+
`set_security`, `set_rate`, `set_timeout`, `set_connect_timeout`,
|
|
114
|
+
`set_user_agent` — and can be chained:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# Free plan
|
|
118
|
+
client = Ipwhois::Client.new
|
|
119
|
+
.set_language('en')
|
|
120
|
+
.set_fields(%w[success country city flag.emoji])
|
|
121
|
+
.set_timeout(8)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# Paid plan
|
|
126
|
+
client = Ipwhois::Client.new('YOUR_API_KEY')
|
|
127
|
+
.set_language('en')
|
|
128
|
+
.set_fields(%w[success country city flag.emoji])
|
|
129
|
+
.set_timeout(8)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Either client behaves the same way at call time — per-call options always
|
|
133
|
+
win over the defaults:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
client.lookup('8.8.8.8') # uses lang=en, the field whitelist, and timeout=8
|
|
137
|
+
client.lookup('1.1.1.1', lang: 'de') # overrides lang for this single call only
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
> ⚠️ When you restrict fields with `set_fields` (or the per-call `:fields`
|
|
141
|
+
> option), the API only returns the fields you ask for. Always include
|
|
142
|
+
> `'success'` in the list if you rely on `info['success']` for error
|
|
143
|
+
> checking — otherwise the field will be missing on responses.
|
|
144
|
+
|
|
145
|
+
> ℹ️ `set_security(true)` requires Business+ and `set_rate(true)` requires
|
|
146
|
+
> Basic+. See the table above for what's available where.
|
|
147
|
+
|
|
148
|
+
## HTTPS Encryption
|
|
149
|
+
|
|
150
|
+
By default, all requests are sent over HTTPS. If you need to disable it (for
|
|
151
|
+
example, in environments without an up-to-date CA bundle), pass `ssl: false`
|
|
152
|
+
to the constructor:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
# Free plan
|
|
156
|
+
client = Ipwhois::Client.new(nil, ssl: false)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
# Paid plan
|
|
161
|
+
client = Ipwhois::Client.new('YOUR_API_KEY', ssl: false)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
> ℹ️ HTTPS is strongly recommended for production traffic — your API key is
|
|
165
|
+
> sent in the query string and would otherwise travel in clear text.
|
|
166
|
+
|
|
167
|
+
## Bulk lookup (Paid plan only)
|
|
168
|
+
|
|
169
|
+
The bulk endpoint sends **up to 100 IPs** in a single GET request. Each
|
|
170
|
+
address counts as one credit. Available on the **Business** and **Unlimited**
|
|
171
|
+
plans.
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
client = Ipwhois::Client.new('YOUR_API_KEY')
|
|
175
|
+
|
|
176
|
+
results = client.bulk_lookup([
|
|
177
|
+
'8.8.8.8',
|
|
178
|
+
'1.1.1.1',
|
|
179
|
+
'208.67.222.222',
|
|
180
|
+
'2c0f:fb50:4003::' # IPv6 is fine — mix freely
|
|
181
|
+
])
|
|
182
|
+
|
|
183
|
+
results.each do |row|
|
|
184
|
+
if row['success'] == false
|
|
185
|
+
# Per-IP errors (e.g. "Invalid IP address") are returned inline,
|
|
186
|
+
# they do NOT raise — the rest of the batch is still usable.
|
|
187
|
+
puts "skip #{row['ip']}: #{row['message']}"
|
|
188
|
+
next
|
|
189
|
+
end
|
|
190
|
+
puts "#{row['ip']} → #{row['country']}"
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
> ℹ️ Bulk requires an API key. Calling `bulk_lookup` without one will fail
|
|
195
|
+
> at the API level.
|
|
196
|
+
|
|
197
|
+
## Error handling
|
|
198
|
+
|
|
199
|
+
**The library never raises.** Every failure — invalid IP, bad API key, rate
|
|
200
|
+
limit, network outage, bad options — comes back inside the response hash
|
|
201
|
+
with `'success' => false` and a `'message'`. Just check `info['success']`
|
|
202
|
+
after every call:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
info = client.lookup('8.8.8.8')
|
|
206
|
+
|
|
207
|
+
unless info['success']
|
|
208
|
+
warn "Lookup failed: #{info['message']}"
|
|
209
|
+
return
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
puts info['country']
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
This means an outage of the ipwhois.io API (or of your server's DNS,
|
|
216
|
+
connection, etc.) will never surface as a fatal error in your application —
|
|
217
|
+
you decide how to react.
|
|
218
|
+
|
|
219
|
+
### Error response fields
|
|
220
|
+
|
|
221
|
+
Every error response contains `'success' => false`, a human-readable
|
|
222
|
+
`'message'`, and an `'error_type'` so you can branch on the category of the
|
|
223
|
+
failure. Some errors include extra fields you can branch on:
|
|
224
|
+
|
|
225
|
+
| Field | When it's present |
|
|
226
|
+
| --------------- | -------------------------------------------------------------------------------------------- |
|
|
227
|
+
| `'success'` | Always — false for error responses (true for successful responses) |
|
|
228
|
+
| `'message'` | Always — human-readable description of what went wrong |
|
|
229
|
+
| `'error_type'` | Always — one of `'api'`, `'network'`, or `'invalid_argument'` |
|
|
230
|
+
| `'http_status'` | On HTTP 4xx / 5xx responses |
|
|
231
|
+
| `'retry_after'` | On HTTP 429 — **free plan only** (the paid endpoint does not send a `Retry-After` header) |
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
info = client.lookup('8.8.8.8')
|
|
235
|
+
|
|
236
|
+
unless info['success']
|
|
237
|
+
if info['http_status'] == 429
|
|
238
|
+
sleep(info['retry_after'] || 60)
|
|
239
|
+
# …retry
|
|
240
|
+
end
|
|
241
|
+
if info['error_type'] == 'network'
|
|
242
|
+
# DNS failure, connection refused, timeout, …
|
|
243
|
+
end
|
|
244
|
+
warn "Error: #{info['message']}"
|
|
245
|
+
return
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Response shape
|
|
250
|
+
|
|
251
|
+
A successful response includes (depending on your plan and selected options):
|
|
252
|
+
|
|
253
|
+
```jsonc
|
|
254
|
+
{
|
|
255
|
+
"ip": "8.8.4.4",
|
|
256
|
+
"success": true,
|
|
257
|
+
"type": "IPv4",
|
|
258
|
+
"continent": "North America",
|
|
259
|
+
"continent_code": "NA",
|
|
260
|
+
"country": "United States",
|
|
261
|
+
"country_code": "US",
|
|
262
|
+
"region": "California",
|
|
263
|
+
"region_code": "CA",
|
|
264
|
+
"city": "Mountain View",
|
|
265
|
+
"latitude": 37.3860517,
|
|
266
|
+
"longitude": -122.0838511,
|
|
267
|
+
"is_eu": false,
|
|
268
|
+
"postal": "94039",
|
|
269
|
+
"calling_code": "1",
|
|
270
|
+
"capital": "Washington D.C.",
|
|
271
|
+
"borders": "CA,MX",
|
|
272
|
+
"flag": {
|
|
273
|
+
"img": "https://cdn.ipwhois.io/flags/us.svg",
|
|
274
|
+
"emoji": "🇺🇸",
|
|
275
|
+
"emoji_unicode": "U+1F1FA U+1F1F8"
|
|
276
|
+
},
|
|
277
|
+
"connection": {
|
|
278
|
+
"asn": 15169,
|
|
279
|
+
"org": "Google LLC",
|
|
280
|
+
"isp": "Google LLC",
|
|
281
|
+
"domain": "google.com"
|
|
282
|
+
},
|
|
283
|
+
"timezone": {
|
|
284
|
+
"id": "America/Los_Angeles",
|
|
285
|
+
"abbr": "PDT",
|
|
286
|
+
"is_dst": true,
|
|
287
|
+
"offset": -25200,
|
|
288
|
+
"utc": "-07:00",
|
|
289
|
+
"current_time": "2026-05-08T14:31:48-07:00"
|
|
290
|
+
},
|
|
291
|
+
"currency": {
|
|
292
|
+
"name": "US Dollar",
|
|
293
|
+
"code": "USD",
|
|
294
|
+
"symbol": "$",
|
|
295
|
+
"plural": "US dollars",
|
|
296
|
+
"exchange_rate": 1
|
|
297
|
+
},
|
|
298
|
+
"security": {
|
|
299
|
+
"anonymous": false,
|
|
300
|
+
"proxy": false,
|
|
301
|
+
"vpn": false,
|
|
302
|
+
"tor": false,
|
|
303
|
+
"hosting": false
|
|
304
|
+
},
|
|
305
|
+
"rate": {
|
|
306
|
+
"limit": 250000,
|
|
307
|
+
"remaining": 50155
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Responses are parsed with the stdlib `JSON` module, so all hashes use **string
|
|
313
|
+
keys** (`info['country']`, not `info[:country]`). Use `Hash#dig` for safe
|
|
314
|
+
access to nested keys: `info.dig('flag', 'emoji')`.
|
|
315
|
+
|
|
316
|
+
For the full field reference, see the [official documentation](https://ipwhois.io/documentation).
|
|
317
|
+
|
|
318
|
+
An **error** response looks like:
|
|
319
|
+
|
|
320
|
+
```jsonc
|
|
321
|
+
{
|
|
322
|
+
"success": false,
|
|
323
|
+
"message": "Rate limit exceeded",
|
|
324
|
+
"error_type": "api", // 'api' / 'network' / 'invalid_argument'
|
|
325
|
+
"http_status": 429, // present for HTTP 4xx / 5xx
|
|
326
|
+
"retry_after": 60 // additionally present on HTTP 429 — free plan only
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Requirements
|
|
331
|
+
|
|
332
|
+
- Ruby **3.0** or newer
|
|
333
|
+
- No runtime gem dependencies (uses stdlib only)
|
|
334
|
+
|
|
335
|
+
## Development
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
git clone https://github.com/IPWhois/ipwhois-ruby.git
|
|
339
|
+
cd ipwhois-ruby
|
|
340
|
+
bundle install
|
|
341
|
+
rake test
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Contributing
|
|
345
|
+
|
|
346
|
+
Issues and pull requests are welcome on
|
|
347
|
+
[GitHub](https://github.com/IPWhois/ipwhois-ruby).
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
[MIT](LICENSE) © ipwhois.io
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'openssl'
|
|
7
|
+
|
|
8
|
+
module Ipwhois
|
|
9
|
+
# Ruby client for the ipwhois.io IP Geolocation API.
|
|
10
|
+
#
|
|
11
|
+
# Quick start
|
|
12
|
+
# -----------
|
|
13
|
+
# # Free plan (no API key, ~1 request/second per client IP)
|
|
14
|
+
# client = Ipwhois::Client.new
|
|
15
|
+
# info = client.lookup('8.8.8.8')
|
|
16
|
+
#
|
|
17
|
+
# # Paid plan (with API key, higher limits, bulk, security data, …)
|
|
18
|
+
# client = Ipwhois::Client.new('YOUR_API_KEY')
|
|
19
|
+
# info = client.lookup('8.8.8.8', lang: 'en', security: true)
|
|
20
|
+
#
|
|
21
|
+
# # Bulk lookup — up to 100 IPs in one call (paid only)
|
|
22
|
+
# list = client.bulk_lookup(['8.8.8.8', '1.1.1.1', '208.67.222.222'])
|
|
23
|
+
#
|
|
24
|
+
# # HTTPS is enabled by default. Pass `ssl: false` to fall back to HTTP.
|
|
25
|
+
#
|
|
26
|
+
# Error handling
|
|
27
|
+
# --------------
|
|
28
|
+
# The library never raises. All errors — invalid input, network failure,
|
|
29
|
+
# API-level errors (bad IP, bad key, rate limit, …) — are returned in the
|
|
30
|
+
# response hash with `'success' => false` and a `'message'`. Just check
|
|
31
|
+
# `info['success']` after every call.
|
|
32
|
+
class Client
|
|
33
|
+
# Free-plan endpoint host (used when no API key is provided).
|
|
34
|
+
HOST_FREE = 'ipwho.is'
|
|
35
|
+
|
|
36
|
+
# Paid-plan endpoint host (used when an API key is provided).
|
|
37
|
+
HOST_PAID = 'ipwhois.pro'
|
|
38
|
+
|
|
39
|
+
# Maximum number of IP addresses allowed in a single bulk request.
|
|
40
|
+
BULK_LIMIT = 100
|
|
41
|
+
|
|
42
|
+
# Languages supported by the `lang` option.
|
|
43
|
+
SUPPORTED_LANGUAGES = %w[en ru de es pt-BR fr zh-CN ja].freeze
|
|
44
|
+
|
|
45
|
+
DEFAULT_TIMEOUT = 10
|
|
46
|
+
DEFAULT_CONNECT_TIMEOUT = 5
|
|
47
|
+
MAX_REDIRECTS = 3
|
|
48
|
+
|
|
49
|
+
# @param api_key [String, nil] Your ipwhois.io API key. Omit (or pass nil)
|
|
50
|
+
# for the free plan.
|
|
51
|
+
# @param options [Hash] Optional defaults applied to every request.
|
|
52
|
+
# Recognised keys: `:lang`, `:fields`, `:security`, `:rate`, `:ssl`,
|
|
53
|
+
# `:timeout`, `:connect_timeout`, `:user_agent`.
|
|
54
|
+
def initialize(api_key = nil, **options)
|
|
55
|
+
@api_key = api_key
|
|
56
|
+
@timeout = options.delete(:timeout) || DEFAULT_TIMEOUT
|
|
57
|
+
@connect_timeout = options.delete(:connect_timeout) || DEFAULT_CONNECT_TIMEOUT
|
|
58
|
+
@user_agent = options.delete(:user_agent) || "ipwhois-ruby/#{Ipwhois::VERSION}"
|
|
59
|
+
@ssl = options.key?(:ssl) ? options.delete(:ssl) != false : true
|
|
60
|
+
@defaults = options
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Look up information for a single IP address.
|
|
64
|
+
#
|
|
65
|
+
# Pass `nil` (or call without arguments) to look up the caller's own
|
|
66
|
+
# public IP, as documented at https://ipwhois.io/documentation.
|
|
67
|
+
#
|
|
68
|
+
# The library never raises — check `result['success']` after every call.
|
|
69
|
+
#
|
|
70
|
+
# @param ip [String, nil] IPv4 or IPv6 address. nil = current IP.
|
|
71
|
+
# @param options [Hash] Per-call options: `:lang`, `:fields`, `:security`,
|
|
72
|
+
# `:rate`.
|
|
73
|
+
# @return [Hash] Decoded JSON response. On any error (API, network, bad
|
|
74
|
+
# input) the hash contains `'success' => false` and `'message'`. The
|
|
75
|
+
# library never raises.
|
|
76
|
+
def lookup(ip = nil, **options)
|
|
77
|
+
error = validate_options(options)
|
|
78
|
+
return error if error
|
|
79
|
+
|
|
80
|
+
path = ip.nil? ? '/' : "/#{escape_path_segment(ip.to_s)}"
|
|
81
|
+
url = build_url(path, options)
|
|
82
|
+
|
|
83
|
+
request(url)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Look up information for multiple IP addresses in a single request.
|
|
87
|
+
#
|
|
88
|
+
# Uses the GET / comma-separated form documented at
|
|
89
|
+
# https://ipwhois.io/documentation/bulk — up to 100 addresses per call.
|
|
90
|
+
# Each address counts as one credit.
|
|
91
|
+
#
|
|
92
|
+
# Available on the Business and Unlimited plans only.
|
|
93
|
+
#
|
|
94
|
+
# Per-IP errors are returned inline with `'success' => false` for the
|
|
95
|
+
# affected entry; the rest of the batch is still usable. If the whole
|
|
96
|
+
# call fails, the response is a single error hash with `'success' => false`
|
|
97
|
+
# instead of an array.
|
|
98
|
+
#
|
|
99
|
+
# @param ips [Array<String>] Up to 100 IPv4/IPv6 addresses (mixable).
|
|
100
|
+
# @param options [Hash] Per-call options (same keys as {#lookup}).
|
|
101
|
+
# @return [Array<Hash>, Hash] Array of per-IP results on success; a single
|
|
102
|
+
# error hash on whole-batch failure. The library never raises.
|
|
103
|
+
def bulk_lookup(ips, **options)
|
|
104
|
+
unless ips.is_a?(Array)
|
|
105
|
+
return {
|
|
106
|
+
'success' => false,
|
|
107
|
+
'message' => 'Bulk lookup requires an Array of IP addresses.',
|
|
108
|
+
'error_type' => 'invalid_argument'
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if ips.empty?
|
|
113
|
+
return {
|
|
114
|
+
'success' => false,
|
|
115
|
+
'message' => 'Bulk lookup requires at least one IP address.',
|
|
116
|
+
'error_type' => 'invalid_argument'
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if ips.size > BULK_LIMIT
|
|
121
|
+
return {
|
|
122
|
+
'success' => false,
|
|
123
|
+
'message' => "Bulk lookup accepts at most #{BULK_LIMIT} IP addresses per call, got #{ips.size}.",
|
|
124
|
+
'error_type' => 'invalid_argument'
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
error = validate_options(options)
|
|
129
|
+
return error if error
|
|
130
|
+
|
|
131
|
+
# The API accepts addresses joined by commas — no URL-encoding of the
|
|
132
|
+
# commas themselves, otherwise the path is misinterpreted.
|
|
133
|
+
joined = ips.map { |ip| escape_path_segment(ip.to_s) }.join(',')
|
|
134
|
+
url = build_url("/bulk/#{joined}", options)
|
|
135
|
+
|
|
136
|
+
request(url)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Set the default language used when none is supplied per call.
|
|
140
|
+
#
|
|
141
|
+
# @param lang [String] One of {SUPPORTED_LANGUAGES}.
|
|
142
|
+
# @return [self]
|
|
143
|
+
def set_language(lang)
|
|
144
|
+
@defaults[:lang] = lang
|
|
145
|
+
self
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Restrict every response to a fixed set of fields by default.
|
|
149
|
+
#
|
|
150
|
+
# Include `'success'` in the list if you rely on `info['success']` for
|
|
151
|
+
# error checking — when `:fields` is set, the API only returns the fields
|
|
152
|
+
# you ask for.
|
|
153
|
+
#
|
|
154
|
+
# @param fields [Array<String>] For example: %w[success country city flag.emoji].
|
|
155
|
+
# @return [self]
|
|
156
|
+
def set_fields(fields)
|
|
157
|
+
@defaults[:fields] = fields
|
|
158
|
+
self
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Enable or disable threat-detection data on every call by default.
|
|
162
|
+
# @return [self]
|
|
163
|
+
def set_security(enabled)
|
|
164
|
+
@defaults[:security] = enabled
|
|
165
|
+
self
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Enable or disable the `rate` block in responses by default.
|
|
169
|
+
# @return [self]
|
|
170
|
+
def set_rate(enabled)
|
|
171
|
+
@defaults[:rate] = enabled
|
|
172
|
+
self
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Set the per-request total timeout in seconds (default: 10).
|
|
176
|
+
# @return [self]
|
|
177
|
+
def set_timeout(seconds)
|
|
178
|
+
@timeout = seconds
|
|
179
|
+
self
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Set the connection timeout in seconds (default: 5).
|
|
183
|
+
# @return [self]
|
|
184
|
+
def set_connect_timeout(seconds)
|
|
185
|
+
@connect_timeout = seconds
|
|
186
|
+
self
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Override the User-Agent header sent with every request.
|
|
190
|
+
# @return [self]
|
|
191
|
+
def set_user_agent(user_agent)
|
|
192
|
+
@user_agent = user_agent
|
|
193
|
+
self
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# ------------------------------------------------------------------
|
|
197
|
+
# Internals
|
|
198
|
+
# ------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
# Validate per-call options. Returns an error hash on the first invalid
|
|
203
|
+
# option, or nil if everything looks OK.
|
|
204
|
+
def validate_options(options)
|
|
205
|
+
merged = @defaults.merge(options)
|
|
206
|
+
|
|
207
|
+
if merged[:lang]
|
|
208
|
+
lang = merged[:lang].to_s
|
|
209
|
+
unless SUPPORTED_LANGUAGES.include?(lang)
|
|
210
|
+
return {
|
|
211
|
+
'success' => false,
|
|
212
|
+
'message' => "Unsupported language \"#{lang}\". Supported: #{SUPPORTED_LANGUAGES.join(', ')}.",
|
|
213
|
+
'error_type' => 'invalid_argument'
|
|
214
|
+
}
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
nil
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Build the full URL for a given path + options.
|
|
222
|
+
def build_url(path, options)
|
|
223
|
+
host = @api_key ? HOST_PAID : HOST_FREE
|
|
224
|
+
|
|
225
|
+
# Per-call options win over defaults.
|
|
226
|
+
merged = @defaults.merge(options)
|
|
227
|
+
|
|
228
|
+
query = []
|
|
229
|
+
query << ['key', @api_key] if @api_key
|
|
230
|
+
query << ['lang', merged[:lang].to_s] if merged[:lang]
|
|
231
|
+
|
|
232
|
+
if merged[:fields]
|
|
233
|
+
fields_value = merged[:fields].is_a?(Array) ? merged[:fields].join(',') : merged[:fields].to_s
|
|
234
|
+
query << ['fields', fields_value]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
query << ['security', '1'] if merged[:security]
|
|
238
|
+
query << ['rate', '1'] if merged[:rate]
|
|
239
|
+
|
|
240
|
+
scheme = @ssl ? 'https' : 'http'
|
|
241
|
+
url = "#{scheme}://#{host}#{path}"
|
|
242
|
+
url += "?#{URI.encode_www_form(query)}" unless query.empty?
|
|
243
|
+
url
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# RFC 3986 percent-encoding for a single path segment — encodes
|
|
247
|
+
# everything outside unreserved chars, in particular the `:` characters
|
|
248
|
+
# in IPv6 addresses, which would otherwise be interpreted as a port
|
|
249
|
+
# separator by URI parsers.
|
|
250
|
+
def escape_path_segment(str)
|
|
251
|
+
str.gsub(/([^A-Za-z0-9\-._~])/) { |c| format('%%%02X', c.ord) }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Perform a GET request and return the decoded JSON body.
|
|
255
|
+
def request(url)
|
|
256
|
+
uri = URI.parse(url)
|
|
257
|
+
|
|
258
|
+
http_response = perform_http_get(uri, MAX_REDIRECTS)
|
|
259
|
+
# On network failure perform_http_get returns a pre-shaped error hash.
|
|
260
|
+
return http_response if http_response.is_a?(Hash)
|
|
261
|
+
|
|
262
|
+
status_code = http_response.code.to_i
|
|
263
|
+
body = http_response.body.to_s
|
|
264
|
+
|
|
265
|
+
decoded =
|
|
266
|
+
if body.empty?
|
|
267
|
+
nil
|
|
268
|
+
else
|
|
269
|
+
begin
|
|
270
|
+
JSON.parse(body)
|
|
271
|
+
rescue JSON::ParserError
|
|
272
|
+
# The ipwhois API always returns JSON. A non-JSON body means
|
|
273
|
+
# something went wrong upstream (gateway error page, captive
|
|
274
|
+
# portal, hijacked response, …) — synthesise an error hash so
|
|
275
|
+
# the caller can handle it the same way as a normal API error.
|
|
276
|
+
snippet = body.gsub(/\s+/, ' ').strip
|
|
277
|
+
snippet = "#{snippet[0, 200]}…" if snippet.length > 200
|
|
278
|
+
return {
|
|
279
|
+
'success' => false,
|
|
280
|
+
'message' => "Invalid JSON returned by ipwhois API (HTTP #{status_code}): #{snippet}",
|
|
281
|
+
'http_status' => status_code,
|
|
282
|
+
'error_type' => 'api'
|
|
283
|
+
}
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Normalise plain values into a hash so the rest of the pipeline can
|
|
288
|
+
# treat the result uniformly. Arrays (bulk responses) are passed
|
|
289
|
+
# through as-is.
|
|
290
|
+
decoded = {} if decoded.nil?
|
|
291
|
+
decoded = { 'value' => decoded } unless decoded.is_a?(Hash) || decoded.is_a?(Array)
|
|
292
|
+
|
|
293
|
+
# For HTTP errors, normalise into a `success => false` hash so the
|
|
294
|
+
# caller doesn't have to inspect HTTP status separately.
|
|
295
|
+
if status_code >= 400
|
|
296
|
+
if decoded.is_a?(Hash) && decoded['success'] == false
|
|
297
|
+
# The API already shaped the error correctly — just enrich it.
|
|
298
|
+
decoded['http_status'] = status_code
|
|
299
|
+
else
|
|
300
|
+
message = (decoded.is_a?(Hash) && decoded['message']) ||
|
|
301
|
+
"HTTP #{status_code} returned by ipwhois API"
|
|
302
|
+
decoded = {
|
|
303
|
+
'success' => false,
|
|
304
|
+
'message' => message,
|
|
305
|
+
'http_status' => status_code
|
|
306
|
+
}
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# `Retry-After` is only emitted by the free-plan endpoint
|
|
310
|
+
# (ipwho.is); the paid endpoint (ipwhois.pro) does not send the
|
|
311
|
+
# header, so don't try to read it there.
|
|
312
|
+
if status_code == 429 && @api_key.nil?
|
|
313
|
+
retry_after = http_response['retry-after']
|
|
314
|
+
decoded['retry_after'] = retry_after.to_i if retry_after
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Tag every API-shaped error (`success => false` returned by the API,
|
|
319
|
+
# on any HTTP status) with `error_type => 'api'` so callers can branch
|
|
320
|
+
# on the category alongside the non-API codes ('network',
|
|
321
|
+
# 'invalid_argument'). HTTP 2xx + success=false bodies (e.g. "Invalid
|
|
322
|
+
# IP address", "Reserved range") are otherwise passed through
|
|
323
|
+
# untouched.
|
|
324
|
+
if decoded.is_a?(Hash) && decoded['success'] == false && !decoded.key?('error_type')
|
|
325
|
+
decoded['error_type'] = 'api'
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
decoded
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Perform the actual HTTP GET. Follows up to `redirects_left` redirects.
|
|
332
|
+
# Returns either a Net::HTTPResponse or a pre-shaped error hash on
|
|
333
|
+
# network/transport failure.
|
|
334
|
+
def perform_http_get(uri, redirects_left)
|
|
335
|
+
use_ssl = (uri.scheme == 'https')
|
|
336
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
337
|
+
http.use_ssl = use_ssl
|
|
338
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER if use_ssl
|
|
339
|
+
http.open_timeout = @connect_timeout
|
|
340
|
+
http.read_timeout = @timeout
|
|
341
|
+
|
|
342
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
343
|
+
req['User-Agent'] = @user_agent
|
|
344
|
+
req['Accept'] = 'application/json'
|
|
345
|
+
|
|
346
|
+
response = http.request(req)
|
|
347
|
+
|
|
348
|
+
if response.is_a?(Net::HTTPRedirection) && redirects_left.positive?
|
|
349
|
+
location = response['location']
|
|
350
|
+
return network_error('redirect without Location header') if location.nil? || location.empty?
|
|
351
|
+
|
|
352
|
+
new_uri = URI.parse(location)
|
|
353
|
+
# Location headers may be relative — resolve against the current URI.
|
|
354
|
+
new_uri = uri + location unless new_uri.absolute?
|
|
355
|
+
return perform_http_get(new_uri, redirects_left - 1)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
response
|
|
359
|
+
rescue Net::OpenTimeout => e
|
|
360
|
+
network_error("connection timeout: #{e.message}")
|
|
361
|
+
rescue Net::ReadTimeout => e
|
|
362
|
+
network_error("read timeout: #{e.message}")
|
|
363
|
+
rescue SocketError => e
|
|
364
|
+
network_error(e.message)
|
|
365
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
366
|
+
network_error("SSL error: #{e.message}")
|
|
367
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH,
|
|
368
|
+
Errno::ENETUNREACH, Errno::ETIMEDOUT, EOFError, IOError => e
|
|
369
|
+
network_error(e.message)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def network_error(message)
|
|
373
|
+
{
|
|
374
|
+
'success' => false,
|
|
375
|
+
'message' => "Network error: #{message}",
|
|
376
|
+
'error_type' => 'network'
|
|
377
|
+
}
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
data/lib/ipwhois.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'ipwhois/version'
|
|
4
|
+
require_relative 'ipwhois/client'
|
|
5
|
+
|
|
6
|
+
# Top-level namespace for the ipwhois.io IP Geolocation API client.
|
|
7
|
+
#
|
|
8
|
+
# Quick start
|
|
9
|
+
# -----------
|
|
10
|
+
# # Free plan (no API key, ~1 request/second per client IP)
|
|
11
|
+
# client = Ipwhois::Client.new
|
|
12
|
+
# info = client.lookup('8.8.8.8')
|
|
13
|
+
#
|
|
14
|
+
# # Paid plan (with API key, higher limits, bulk, security data, …)
|
|
15
|
+
# client = Ipwhois::Client.new('YOUR_API_KEY')
|
|
16
|
+
# info = client.lookup('8.8.8.8', lang: 'en', security: true)
|
|
17
|
+
#
|
|
18
|
+
# # Bulk lookup — up to 100 IPs in one call (paid only)
|
|
19
|
+
# list = client.bulk_lookup(['8.8.8.8', '1.1.1.1', '208.67.222.222'])
|
|
20
|
+
#
|
|
21
|
+
# # HTTPS is enabled by default. Pass `ssl: false` to fall back to HTTP.
|
|
22
|
+
#
|
|
23
|
+
# Error handling
|
|
24
|
+
# --------------
|
|
25
|
+
# The library never raises. All errors — invalid input, network failure,
|
|
26
|
+
# API-level errors (bad IP, bad key, rate limit, …) — are returned in the
|
|
27
|
+
# response hash with `'success' => false` and a `'message'`. Just check
|
|
28
|
+
# `info['success']` after every call.
|
|
29
|
+
module Ipwhois
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ipwhois
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ipwhois.io
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-17 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
description: Simple, dependency-free Ruby client for the ipwhois.io IP Geolocation
|
|
42
|
+
API. Supports single and bulk IP lookups (IPv4 and IPv6), free and paid plans, localisation,
|
|
43
|
+
field selection, threat detection and rate-limit info.
|
|
44
|
+
email:
|
|
45
|
+
- support@ipwhois.io
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE
|
|
52
|
+
- README.md
|
|
53
|
+
- lib/ipwhois.rb
|
|
54
|
+
- lib/ipwhois/client.rb
|
|
55
|
+
- lib/ipwhois/version.rb
|
|
56
|
+
homepage: https://ipwhois.io
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
homepage_uri: https://ipwhois.io
|
|
61
|
+
source_code_uri: https://github.com/IPWhois/ipwhois-ruby
|
|
62
|
+
bug_tracker_uri: https://github.com/IPWhois/ipwhois-ruby/issues
|
|
63
|
+
documentation_uri: https://ipwhois.io/documentation
|
|
64
|
+
changelog_uri: https://github.com/IPWhois/ipwhois-ruby/blob/main/CHANGELOG.md
|
|
65
|
+
allowed_push_host: https://rubygems.org
|
|
66
|
+
rubygems_mfa_required: 'true'
|
|
67
|
+
post_install_message:
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 3.0.0
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 3.0.3.1
|
|
83
|
+
signing_key:
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: Official Ruby client for the ipwhois.io IP Geolocation API.
|
|
86
|
+
test_files: []
|