ipdata 0.1.5
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/LICENSE +21 -0
- data/README.md +235 -0
- data/lib/ipdata/cache.rb +71 -0
- data/lib/ipdata/client.rb +151 -0
- data/lib/ipdata/errors.rb +47 -0
- data/lib/ipdata/response.rb +86 -0
- data/lib/ipdata/version.rb +5 -0
- data/lib/ipdata.rb +7 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ecd1e1860fb3d422c8aea50274e10528dd45d12c4555de9585e7af64f06de450
|
|
4
|
+
data.tar.gz: f7ad0dcf1f3fd4a526e5ebe64b901c848f4c5cab432ebe33d0a897f72f9c500d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5c3e379a60cd06fc5a5e38aca4680d2014c6b2324bd2587cb108310c0c1356cfc294bd2082ed1d3278e4b7950e5df1c0ff0d7da89be6f40d2b307e63eebdf0e9
|
|
7
|
+
data.tar.gz: 18f91231a4c26f1c3ba5f2e86436eff0ca22a2b8078f9749af994ace74de4707c4c95d0416344ed658ca186504bdbb3ebfbdec669466ce00d8c6c1a095ad8377
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 IPData
|
|
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,235 @@
|
|
|
1
|
+
# IPData Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby client for the [ipdata.co](https://ipdata.co) IP geolocation API. Look up geolocation, threat intelligence, ASN, company, carrier, currency, timezone, and language data for any IP address.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "ipdata"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install directly:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
gem install ipdata
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
Get a free API key at [ipdata.co/sign-up](https://ipdata.co/sign-up.html) (1,500 requests/day).
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "ipdata"
|
|
31
|
+
|
|
32
|
+
client = IPData::Client.new("YOUR_API_KEY")
|
|
33
|
+
|
|
34
|
+
# Look up an IP address
|
|
35
|
+
response = client.lookup("8.8.8.8")
|
|
36
|
+
|
|
37
|
+
response.ip # => "8.8.8.8"
|
|
38
|
+
response.city # => "Mountain View"
|
|
39
|
+
response.country_name # => "United States"
|
|
40
|
+
response.latitude # => 37.386
|
|
41
|
+
response.longitude # => -122.0838
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Look Up Your Own IP
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
response = client.lookup
|
|
50
|
+
response.ip # => your public IP
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Look Up a Specific IP
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
response = client.lookup("1.1.1.1")
|
|
57
|
+
response.country_name # => "Australia"
|
|
58
|
+
response.asn.name # => "Cloudflare, Inc."
|
|
59
|
+
response.asn.asn # => "AS13335"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Select Specific Fields
|
|
63
|
+
|
|
64
|
+
Reduce payload by requesting only the fields you need:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
response = client.lookup("8.8.8.8", fields: ["ip", "city", "country_name"])
|
|
68
|
+
response.city # => "Mountain View"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Extract a Single Field
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
city = client.lookup("8.8.8.8", select_field: "city")
|
|
75
|
+
# => "Mountain View"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Bulk Lookup
|
|
79
|
+
|
|
80
|
+
Look up multiple IPs in a single request (max 100):
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
responses = client.bulk_lookup(["8.8.8.8", "1.1.1.1"])
|
|
84
|
+
responses.each do |r|
|
|
85
|
+
puts "#{r.ip}: #{r.country_name}"
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Bulk lookup also supports field filtering:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
responses = client.bulk_lookup(
|
|
93
|
+
["8.8.8.8", "1.1.1.1"],
|
|
94
|
+
fields: ["ip", "country_name", "city"]
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### EU Endpoint (GDPR Compliant)
|
|
99
|
+
|
|
100
|
+
Route requests through EU data centers only:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
client = IPData::Client.eu("YOUR_API_KEY")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Caching
|
|
107
|
+
|
|
108
|
+
Enable an in-memory LRU cache to reduce API calls:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
cache = IPData::Cache.new(max_size: 1024, ttl: 3600) # 1h TTL
|
|
112
|
+
client = IPData::Client.new("YOUR_API_KEY", cache: cache)
|
|
113
|
+
|
|
114
|
+
client.lookup("8.8.8.8") # hits API
|
|
115
|
+
client.lookup("8.8.8.8") # served from cache
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Custom Timeout
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
client = IPData::Client.new("YOUR_API_KEY", timeout: 10) # 10 seconds
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Response Fields
|
|
125
|
+
|
|
126
|
+
All responses are wrapped in `IPData::Response` objects that support both dot-notation and hash-style access:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
response = client.lookup("8.8.8.8")
|
|
130
|
+
|
|
131
|
+
# Dot notation
|
|
132
|
+
response.ip
|
|
133
|
+
response.city
|
|
134
|
+
response.region
|
|
135
|
+
response.country_name
|
|
136
|
+
response.country_code
|
|
137
|
+
response.continent_name
|
|
138
|
+
response.latitude
|
|
139
|
+
response.longitude
|
|
140
|
+
response.is_eu
|
|
141
|
+
response.postal
|
|
142
|
+
response.calling_code
|
|
143
|
+
response.flag
|
|
144
|
+
response.emoji_flag
|
|
145
|
+
response.emoji_unicode
|
|
146
|
+
|
|
147
|
+
# Nested objects
|
|
148
|
+
response.asn.asn # => "AS15169"
|
|
149
|
+
response.asn.name # => "Google LLC"
|
|
150
|
+
response.asn.domain # => "google.com"
|
|
151
|
+
response.asn.route # => "8.8.8.0/24"
|
|
152
|
+
response.asn.type # => "business"
|
|
153
|
+
|
|
154
|
+
response.company.name
|
|
155
|
+
response.company.domain
|
|
156
|
+
response.company.network
|
|
157
|
+
response.company.type
|
|
158
|
+
|
|
159
|
+
response.carrier.name
|
|
160
|
+
response.carrier.mcc
|
|
161
|
+
response.carrier.mnc
|
|
162
|
+
|
|
163
|
+
response.currency.name
|
|
164
|
+
response.currency.code
|
|
165
|
+
response.currency.symbol
|
|
166
|
+
response.currency.native
|
|
167
|
+
response.currency.plural
|
|
168
|
+
|
|
169
|
+
response.time_zone.name
|
|
170
|
+
response.time_zone.abbr
|
|
171
|
+
response.time_zone.offset
|
|
172
|
+
response.time_zone.is_dst
|
|
173
|
+
response.time_zone.current_time
|
|
174
|
+
|
|
175
|
+
response.threat.is_tor
|
|
176
|
+
response.threat.is_vpn
|
|
177
|
+
response.threat.is_icloud_relay
|
|
178
|
+
response.threat.is_proxy
|
|
179
|
+
response.threat.is_datacenter
|
|
180
|
+
response.threat.is_anonymous
|
|
181
|
+
response.threat.is_known_attacker
|
|
182
|
+
response.threat.is_known_abuser
|
|
183
|
+
response.threat.is_threat
|
|
184
|
+
response.threat.is_bogon
|
|
185
|
+
response.threat.blocklists
|
|
186
|
+
response.threat.scores
|
|
187
|
+
|
|
188
|
+
# Languages (array)
|
|
189
|
+
response.languages.first.name
|
|
190
|
+
response.languages.first.native
|
|
191
|
+
response.languages.first.code
|
|
192
|
+
|
|
193
|
+
# Hash-style access
|
|
194
|
+
response["city"]
|
|
195
|
+
response[:country_code]
|
|
196
|
+
|
|
197
|
+
# Raw hash
|
|
198
|
+
response.to_h
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Error Handling
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
begin
|
|
205
|
+
client.lookup("8.8.8.8")
|
|
206
|
+
rescue IPData::AuthenticationError => e
|
|
207
|
+
# Missing API key (401)
|
|
208
|
+
puts "Auth error: #{e.message} (#{e.status_code})"
|
|
209
|
+
rescue IPData::ForbiddenError => e
|
|
210
|
+
# Invalid key or quota exceeded (403)
|
|
211
|
+
puts "Forbidden: #{e.message} (#{e.status_code})"
|
|
212
|
+
rescue IPData::BadRequestError => e
|
|
213
|
+
# Invalid IP or bad request (400)
|
|
214
|
+
puts "Bad request: #{e.message} (#{e.status_code})"
|
|
215
|
+
rescue IPData::Error => e
|
|
216
|
+
# Any other API error
|
|
217
|
+
puts "Error: #{e.message} (#{e.status_code})"
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Requirements
|
|
222
|
+
|
|
223
|
+
- Ruby >= 3.0
|
|
224
|
+
- No runtime dependencies (uses only Ruby stdlib)
|
|
225
|
+
|
|
226
|
+
## Development
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
bundle install
|
|
230
|
+
bundle exec rake test
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
[MIT](LICENSE)
|
data/lib/ipdata/cache.rb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module IPData
|
|
6
|
+
# Thread-safe LRU cache with TTL expiration.
|
|
7
|
+
#
|
|
8
|
+
# Used to cache IP lookup results and avoid redundant API calls.
|
|
9
|
+
# Uses Ruby's stdlib Monitor for thread safety.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# cache = IPData::Cache.new(max_size: 1024, ttl: 3600)
|
|
13
|
+
# client = IPData::Client.new("KEY", cache: cache)
|
|
14
|
+
class Cache
|
|
15
|
+
Entry = Struct.new(:value, :expires_at)
|
|
16
|
+
|
|
17
|
+
DEFAULT_MAX_SIZE = 4096
|
|
18
|
+
DEFAULT_TTL = 86_400 # 24 hours
|
|
19
|
+
|
|
20
|
+
def initialize(max_size: DEFAULT_MAX_SIZE, ttl: DEFAULT_TTL)
|
|
21
|
+
@max_size = max_size
|
|
22
|
+
@ttl = ttl
|
|
23
|
+
@store = {}
|
|
24
|
+
@monitor = Monitor.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Retrieve a cached value. Returns nil if missing or expired.
|
|
28
|
+
def get(key)
|
|
29
|
+
@monitor.synchronize do
|
|
30
|
+
entry = @store[key]
|
|
31
|
+
return nil unless entry
|
|
32
|
+
|
|
33
|
+
if Time.now > entry.expires_at
|
|
34
|
+
@store.delete(key)
|
|
35
|
+
return nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Move to end (most recently used) by re-inserting
|
|
39
|
+
@store.delete(key)
|
|
40
|
+
@store[key] = entry
|
|
41
|
+
entry.value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Store a value in the cache. Evicts the least recently used entry
|
|
46
|
+
# if the cache is at capacity.
|
|
47
|
+
def set(key, value)
|
|
48
|
+
@monitor.synchronize do
|
|
49
|
+
@store.delete(key) # remove if exists (re-insert at end)
|
|
50
|
+
|
|
51
|
+
# Evict LRU entry if at capacity
|
|
52
|
+
if @store.size >= @max_size
|
|
53
|
+
lru_key = @store.keys.first
|
|
54
|
+
@store.delete(lru_key)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@store[key] = Entry.new(value, Time.now + @ttl)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Remove all entries.
|
|
62
|
+
def clear
|
|
63
|
+
@monitor.synchronize { @store.clear }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Number of entries currently cached.
|
|
67
|
+
def size
|
|
68
|
+
@monitor.synchronize { @store.size }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module IPData
|
|
8
|
+
# HTTP client for the ipdata.co API.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# client = IPData::Client.new("YOUR_API_KEY")
|
|
12
|
+
# response = client.lookup("8.8.8.8")
|
|
13
|
+
# response.city # => "Mountain View"
|
|
14
|
+
#
|
|
15
|
+
# @example EU endpoint (GDPR compliant)
|
|
16
|
+
# client = IPData::Client.eu("YOUR_API_KEY")
|
|
17
|
+
#
|
|
18
|
+
# @example With caching
|
|
19
|
+
# cache = IPData::Cache.new(max_size: 1024, ttl: 3600)
|
|
20
|
+
# client = IPData::Client.new("YOUR_API_KEY", cache: cache)
|
|
21
|
+
class Client
|
|
22
|
+
BASE_URL = "https://api.ipdata.co"
|
|
23
|
+
EU_BASE_URL = "https://eu-api.ipdata.co"
|
|
24
|
+
|
|
25
|
+
# @param api_key [String] Your ipdata API key
|
|
26
|
+
# @param cache [IPData::Cache, nil] Optional LRU cache instance
|
|
27
|
+
# @param timeout [Integer] HTTP timeout in seconds (default: 30)
|
|
28
|
+
# @param base_url [String, nil] Override the base URL (default: global endpoint)
|
|
29
|
+
def initialize(api_key, cache: nil, timeout: 30, base_url: nil)
|
|
30
|
+
raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
|
|
31
|
+
|
|
32
|
+
@api_key = api_key
|
|
33
|
+
@cache = cache
|
|
34
|
+
@timeout = timeout
|
|
35
|
+
@base_uri = URI.parse(base_url || BASE_URL)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Create a client configured for the EU endpoint.
|
|
39
|
+
#
|
|
40
|
+
# @param api_key [String] Your ipdata API key
|
|
41
|
+
# @param opts [Hash] Additional options passed to {#initialize}
|
|
42
|
+
# @return [Client]
|
|
43
|
+
def self.eu(api_key, **opts)
|
|
44
|
+
new(api_key, base_url: EU_BASE_URL, **opts)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Look up geolocation data for an IP address.
|
|
48
|
+
#
|
|
49
|
+
# @param ip [String, nil] IP address to look up (nil for caller's own IP)
|
|
50
|
+
# @param fields [Array<String>, nil] Specific fields to return
|
|
51
|
+
# @param select_field [String, nil] A single field to extract (returns raw value)
|
|
52
|
+
# @return [Response, Object] Response object, or raw value if select_field is used
|
|
53
|
+
def lookup(ip = nil, fields: nil, select_field: nil)
|
|
54
|
+
if select_field
|
|
55
|
+
path = "/#{ip}/#{select_field}"
|
|
56
|
+
data = get(path)
|
|
57
|
+
return data
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
cache_key = build_cache_key(ip, fields)
|
|
61
|
+
if @cache
|
|
62
|
+
cached = @cache.get(cache_key)
|
|
63
|
+
return cached if cached
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
path = ip ? "/#{ip}" : "/"
|
|
67
|
+
params = {}
|
|
68
|
+
params["fields"] = fields.join(",") if fields && !fields.empty?
|
|
69
|
+
|
|
70
|
+
data = get(path, params)
|
|
71
|
+
response = Response.new(data)
|
|
72
|
+
|
|
73
|
+
@cache&.set(cache_key, response)
|
|
74
|
+
response
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Look up geolocation data for multiple IP addresses.
|
|
78
|
+
#
|
|
79
|
+
# @param ips [Array<String>] IP addresses (max 100)
|
|
80
|
+
# @param fields [Array<String>, nil] Specific fields to return
|
|
81
|
+
# @return [Array<Response>]
|
|
82
|
+
# @raise [ArgumentError] if more than 100 IPs are provided
|
|
83
|
+
def bulk_lookup(ips, fields: nil)
|
|
84
|
+
raise ArgumentError, "ips must be an Array" unless ips.is_a?(Array)
|
|
85
|
+
raise ArgumentError, "maximum 100 IPs per bulk request" if ips.size > 100
|
|
86
|
+
|
|
87
|
+
params = {}
|
|
88
|
+
params["fields"] = fields.join(",") if fields && !fields.empty?
|
|
89
|
+
|
|
90
|
+
data = post("/bulk", ips, params)
|
|
91
|
+
data.map { |entry| Response.new(entry) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def user_agent
|
|
97
|
+
@user_agent ||= "ruby-ipdata/#{VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_cache_key(ip, fields)
|
|
101
|
+
key = ip || "_self"
|
|
102
|
+
key = "#{key}:#{fields.sort.join(",")}" if fields && !fields.empty?
|
|
103
|
+
key
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_uri(path, params = {})
|
|
107
|
+
uri = @base_uri.dup
|
|
108
|
+
uri.path = path
|
|
109
|
+
query_params = { "api-key" => @api_key }.merge(params)
|
|
110
|
+
uri.query = URI.encode_www_form(query_params)
|
|
111
|
+
uri
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def get(path, params = {})
|
|
115
|
+
uri = build_uri(path, params)
|
|
116
|
+
|
|
117
|
+
request = Net::HTTP::Get.new(uri)
|
|
118
|
+
request["Accept"] = "application/json"
|
|
119
|
+
request["User-Agent"] = user_agent
|
|
120
|
+
|
|
121
|
+
execute(uri, request)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def post(path, body, params = {})
|
|
125
|
+
uri = build_uri(path, params)
|
|
126
|
+
|
|
127
|
+
request = Net::HTTP::Post.new(uri)
|
|
128
|
+
request["Accept"] = "application/json"
|
|
129
|
+
request["Content-Type"] = "application/json"
|
|
130
|
+
request["User-Agent"] = user_agent
|
|
131
|
+
request.body = JSON.generate(body)
|
|
132
|
+
|
|
133
|
+
execute(uri, request)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def execute(uri, request)
|
|
137
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
138
|
+
http.use_ssl = true
|
|
139
|
+
http.open_timeout = @timeout
|
|
140
|
+
http.read_timeout = @timeout
|
|
141
|
+
|
|
142
|
+
response = http.request(request)
|
|
143
|
+
|
|
144
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
145
|
+
raise Error.from_response(response.code.to_i, response.body)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
JSON.parse(response.body)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module IPData
|
|
6
|
+
# Base error class for all ipdata API errors.
|
|
7
|
+
# Carries the HTTP status code and API error message.
|
|
8
|
+
class Error < StandardError
|
|
9
|
+
attr_reader :status_code
|
|
10
|
+
|
|
11
|
+
def initialize(message, status_code = nil)
|
|
12
|
+
@status_code = status_code
|
|
13
|
+
super(message)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Build the appropriate error subclass from an HTTP response.
|
|
17
|
+
def self.from_response(status, body)
|
|
18
|
+
message = parse_message(body) || "API request failed"
|
|
19
|
+
klass = case status
|
|
20
|
+
when 400 then BadRequestError
|
|
21
|
+
when 401 then AuthenticationError
|
|
22
|
+
when 403 then ForbiddenError
|
|
23
|
+
else Error
|
|
24
|
+
end
|
|
25
|
+
klass.new(message, status)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.parse_message(body)
|
|
29
|
+
return nil if body.nil? || body.empty?
|
|
30
|
+
|
|
31
|
+
data = JSON.parse(body)
|
|
32
|
+
data["message"]
|
|
33
|
+
rescue JSON::ParserError
|
|
34
|
+
body
|
|
35
|
+
end
|
|
36
|
+
private_class_method :parse_message
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Raised when the API key is missing (HTTP 401).
|
|
40
|
+
class AuthenticationError < Error; end
|
|
41
|
+
|
|
42
|
+
# Raised when the API key is invalid or quota is exceeded (HTTP 403).
|
|
43
|
+
class ForbiddenError < Error; end
|
|
44
|
+
|
|
45
|
+
# Raised for invalid IP addresses or malformed requests (HTTP 400).
|
|
46
|
+
class BadRequestError < Error; end
|
|
47
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module IPData
|
|
6
|
+
# Lightweight wrapper around the parsed API JSON response.
|
|
7
|
+
#
|
|
8
|
+
# Provides both dot-notation and hash-style access:
|
|
9
|
+
# response.city # => "Mountain View"
|
|
10
|
+
# response["city"] # => "Mountain View"
|
|
11
|
+
# response[:city] # => "Mountain View"
|
|
12
|
+
# response.asn.name # => "Google LLC"
|
|
13
|
+
# response.threat.is_vpn # => false
|
|
14
|
+
#
|
|
15
|
+
# Nested hashes are recursively wrapped as Response objects.
|
|
16
|
+
# Arrays of hashes (e.g. languages, blocklists) are mapped to Response arrays.
|
|
17
|
+
class Response
|
|
18
|
+
def initialize(data)
|
|
19
|
+
@data = data || {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Hash-style access. Accepts string or symbol keys.
|
|
23
|
+
def [](key)
|
|
24
|
+
wrap(@data[key.to_s])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns the underlying raw Hash.
|
|
28
|
+
def to_h
|
|
29
|
+
@data
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Serialize back to JSON.
|
|
33
|
+
def to_json(*)
|
|
34
|
+
JSON.generate(@data)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_s
|
|
38
|
+
@data.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def inspect
|
|
42
|
+
"#<#{self.class} #{@data.inspect}>"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns true if the key exists in the response data.
|
|
46
|
+
def key?(key)
|
|
47
|
+
@data.key?(key.to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Iterate over key-value pairs.
|
|
51
|
+
def each(&block)
|
|
52
|
+
@data.each(&block)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ==(other)
|
|
56
|
+
case other
|
|
57
|
+
when Response then @data == other.to_h
|
|
58
|
+
when Hash then @data == other
|
|
59
|
+
else false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def wrap(value)
|
|
66
|
+
case value
|
|
67
|
+
when Hash then Response.new(value)
|
|
68
|
+
when Array then value.map { |v| wrap(v) }
|
|
69
|
+
else value
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def method_missing(name, *args)
|
|
74
|
+
key = name.to_s
|
|
75
|
+
if @data.key?(key)
|
|
76
|
+
wrap(@data[key])
|
|
77
|
+
else
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def respond_to_missing?(name, include_private = false)
|
|
83
|
+
@data.key?(name.to_s) || super
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/ipdata.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ipdata
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.5
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- IPData
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-23 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Official Ruby SDK for the ipdata.co API. Look up geolocation, threat
|
|
14
|
+
intelligence, ASN, company, carrier, currency, timezone and language data for any
|
|
15
|
+
IP address.
|
|
16
|
+
email:
|
|
17
|
+
- support@ipdata.co
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/ipdata.rb
|
|
25
|
+
- lib/ipdata/cache.rb
|
|
26
|
+
- lib/ipdata/client.rb
|
|
27
|
+
- lib/ipdata/errors.rb
|
|
28
|
+
- lib/ipdata/response.rb
|
|
29
|
+
- lib/ipdata/version.rb
|
|
30
|
+
homepage: https://github.com/ipdata/ruby
|
|
31
|
+
licenses:
|
|
32
|
+
- MIT
|
|
33
|
+
metadata:
|
|
34
|
+
homepage_uri: https://github.com/ipdata/ruby
|
|
35
|
+
source_code_uri: https://github.com/ipdata/ruby
|
|
36
|
+
changelog_uri: https://github.com/ipdata/ruby/blob/main/CHANGELOG.md
|
|
37
|
+
bug_tracker_uri: https://github.com/ipdata/ruby/issues
|
|
38
|
+
documentation_uri: https://docs.ipdata.co
|
|
39
|
+
post_install_message:
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
require_paths:
|
|
42
|
+
- lib
|
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.0'
|
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
requirements: []
|
|
54
|
+
rubygems_version: 3.5.22
|
|
55
|
+
signing_key:
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: Ruby client for the ipdata IP geolocation API
|
|
58
|
+
test_files: []
|