ipapi-rb 1.0.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/README.md +325 -0
- data/bin/ipapi +140 -0
- data/ipapi-rb.gemspec +41 -0
- data/lib/ip_api/error_result.rb +40 -0
- data/lib/ip_api/helpers.rb +8 -0
- data/lib/ip_api/module_methods.rb +19 -0
- data/lib/ip_api/query_options.rb +60 -0
- data/lib/ip_api/query_request.rb +26 -0
- data/lib/ip_api/query_result.rb +64 -0
- data/lib/ip_api/request.rb +32 -0
- data/lib/ip_api/response_methods.rb +12 -0
- data/lib/ip_api/version.rb +3 -0
- data/lib/ip_api.rb +22 -0
- data/lib/ipapi-rb.rb +1 -0
- metadata +131 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f349f84933eea258dba2b8e92e17f5ed04ac800998135182da92c17d6222724c
|
|
4
|
+
data.tar.gz: cd78e6d317b289099ce5cfd806eb94cda1f4a10d4cfc8071e15c0c0ab06b92f3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5ab6b4f1d3d25bd53fde29cfa798e32b8fc22c88c0cf07ee5285440ad7dfe631811116da8be4fdc5042c09b77eddce0743293897e1d82cbe45a9359aef63efc9
|
|
7
|
+
data.tar.gz: 536ce8dee48bacda8d431fd8dcdda67d21edd0330bf0d54f1a376fa8384e76b0077ba1a929143bf8226045c0f6572f114037928d56d6450717ee06c9d918ef82
|
data/README.md
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# IpApi
|
|
2
|
+
|
|
3
|
+
The **IpApi** gem provides a Ruby interface to the [IP-API.com](https://ip-api.com) geolocation API, enabling IP address and domain lookups to retrieve location, network, and connection information.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
require 'ip_api'
|
|
7
|
+
|
|
8
|
+
response = IpApi.query( '8.8.8.8' )
|
|
9
|
+
if response.success?
|
|
10
|
+
result = response.result
|
|
11
|
+
puts "#{ result.city }, #{ result.country }" # => "Ashburn, United States"
|
|
12
|
+
puts result.isp # => "Google LLC"
|
|
13
|
+
puts result.coordinates # => [39.03, -77.5]
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Table of Contents
|
|
20
|
+
|
|
21
|
+
- [Installation](#installation)
|
|
22
|
+
- [Command Line](#command-line)
|
|
23
|
+
- [Quick Start](#quick-start)
|
|
24
|
+
- [Free vs Pro](#free-vs-pro)
|
|
25
|
+
- [Options](#options)
|
|
26
|
+
- [Fields](#fields)
|
|
27
|
+
- [Language](#language)
|
|
28
|
+
- [Response](#response)
|
|
29
|
+
- [Location Fields](#location-fields)
|
|
30
|
+
- [Network Fields](#network-fields)
|
|
31
|
+
- [Connection Fields](#connection-fields)
|
|
32
|
+
- [Helper Methods](#helper-methods)
|
|
33
|
+
- [Error Handling](#error-handling)
|
|
34
|
+
- [Connections](#connections)
|
|
35
|
+
- [License](#license)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Add this line to your application's Gemfile:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
gem 'ipapi-rb'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then execute:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
bundle install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or install it directly:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
gem install ipapi-rb
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Command Line
|
|
60
|
+
|
|
61
|
+
The gem includes an `ipapi` command for quick lookups from the terminal:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
ipapi 8.8.8.8
|
|
65
|
+
ipapi google.com
|
|
66
|
+
ipapi # Look up your own IP
|
|
67
|
+
ipapi -f country,city 8.8.8.8 # Specific fields only
|
|
68
|
+
ipapi -l de 8.8.8.8 # German localization
|
|
69
|
+
ipapi -j 8.8.8.8 # Raw JSON output
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Set your Pro API key via environment variable for HTTPS and unlimited requests:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export IPAPI_API_KEY=your_api_key
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
The simplest way to use the gem is through the module-level convenience method:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
require 'ip_api'
|
|
84
|
+
|
|
85
|
+
# Query an IP address
|
|
86
|
+
response = IpApi.query( '8.8.8.8' )
|
|
87
|
+
if response.success?
|
|
88
|
+
result = response.result
|
|
89
|
+
puts result.country # => "United States"
|
|
90
|
+
puts result.city # => "Ashburn"
|
|
91
|
+
puts result.isp # => "Google LLC"
|
|
92
|
+
puts result.hosting? # => true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Query a domain
|
|
96
|
+
response = IpApi.query( 'google.com' )
|
|
97
|
+
|
|
98
|
+
# Query your own IP (no argument)
|
|
99
|
+
response = IpApi.query
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
For more control, instantiate request objects directly:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
request = IpApi::QueryRequest.new( api_key: ENV[ 'IPAPI_API_KEY' ] )
|
|
106
|
+
|
|
107
|
+
options = IpApi::QueryOptions.build do
|
|
108
|
+
fields [ :country, :city, :isp, :proxy, :hosting ]
|
|
109
|
+
lang :de
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
response = request.submit( '8.8.8.8', options )
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Free vs Pro
|
|
118
|
+
|
|
119
|
+
The gem automatically selects the appropriate endpoint based on whether an API key is configured:
|
|
120
|
+
|
|
121
|
+
| Feature | Free | Pro |
|
|
122
|
+
|---------|------|-----|
|
|
123
|
+
| **Endpoint** | `http://ip-api.com` | `https://pro.ip-api.com` |
|
|
124
|
+
| **SSL/HTTPS** | No | Yes |
|
|
125
|
+
| **Rate Limit** | 45 requests/minute | Unlimited |
|
|
126
|
+
| **Commercial Use** | No | Yes |
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
# Free (no API key)
|
|
130
|
+
response = IpApi.query( '8.8.8.8' )
|
|
131
|
+
|
|
132
|
+
# Pro (with API key)
|
|
133
|
+
IpApi.api_key ENV[ 'IPAPI_API_KEY' ]
|
|
134
|
+
response = IpApi.query( '8.8.8.8' )
|
|
135
|
+
|
|
136
|
+
# Or pass directly to request
|
|
137
|
+
request = IpApi::QueryRequest.new( api_key: 'your-key' )
|
|
138
|
+
response = request.submit( '8.8.8.8' )
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Options
|
|
144
|
+
|
|
145
|
+
### Fields
|
|
146
|
+
|
|
147
|
+
Control which fields are returned to reduce bandwidth:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
options = IpApi::QueryOptions.build do
|
|
151
|
+
fields [ :status, :country, :city, :lat, :lon ]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
response = IpApi.query( '8.8.8.8', options )
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Available fields (use snake_case in Ruby):
|
|
158
|
+
|
|
159
|
+
| Field | Description |
|
|
160
|
+
|-------|-------------|
|
|
161
|
+
| `:status` | Request status (`success` or `fail`) |
|
|
162
|
+
| `:message` | Error message (when status is `fail`) |
|
|
163
|
+
| `:query` | IP address used for the query |
|
|
164
|
+
| `:continent` | Continent name |
|
|
165
|
+
| `:continent_code` | Two-letter continent code |
|
|
166
|
+
| `:country` | Country name |
|
|
167
|
+
| `:country_code` | ISO 3166-1 alpha-2 country code |
|
|
168
|
+
| `:region` | Region/state short code |
|
|
169
|
+
| `:region_name` | Region/state full name |
|
|
170
|
+
| `:city` | City name |
|
|
171
|
+
| `:district` | District (subdivision of city) |
|
|
172
|
+
| `:zip` | Zip/postal code |
|
|
173
|
+
| `:lat` | Latitude |
|
|
174
|
+
| `:lon` | Longitude |
|
|
175
|
+
| `:timezone` | Timezone identifier |
|
|
176
|
+
| `:offset` | UTC offset in seconds |
|
|
177
|
+
| `:currency` | National currency code |
|
|
178
|
+
| `:isp` | ISP name |
|
|
179
|
+
| `:org` | Organization name |
|
|
180
|
+
| `:as` | AS number and organization |
|
|
181
|
+
| `:as_name` | AS name |
|
|
182
|
+
| `:reverse` | Reverse DNS (can delay response) |
|
|
183
|
+
| `:mobile` | Mobile connection flag |
|
|
184
|
+
| `:proxy` | Proxy/VPN/Tor flag |
|
|
185
|
+
| `:hosting` | Hosting/datacenter flag |
|
|
186
|
+
|
|
187
|
+
### Language
|
|
188
|
+
|
|
189
|
+
Localize the `country`, `region_name`, and `city` fields:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
options = IpApi::QueryOptions.build do
|
|
193
|
+
lang :de # German
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
response = IpApi.query( '8.8.8.8', options )
|
|
197
|
+
puts response.result.country # => "Vereinigte Staaten"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Supported languages: `:en`, `:de`, `:es`, `:fr`, `:ja`, `:ru`, `:'pt-BR'`, `:'zh-CN'`
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Response
|
|
205
|
+
|
|
206
|
+
### Location Fields
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
result = response.result
|
|
210
|
+
|
|
211
|
+
result.continent # => "North America"
|
|
212
|
+
result.continent_code # => "NA"
|
|
213
|
+
result.country # => "United States"
|
|
214
|
+
result.country_code # => "US"
|
|
215
|
+
result.region # => "VA"
|
|
216
|
+
result.region_name # => "Virginia"
|
|
217
|
+
result.city # => "Ashburn"
|
|
218
|
+
result.district # => nil
|
|
219
|
+
result.zip # => "20149"
|
|
220
|
+
result.lat # => 39.03
|
|
221
|
+
result.lon # => -77.5
|
|
222
|
+
result.timezone # => "America/New_York"
|
|
223
|
+
result.offset # => -18000
|
|
224
|
+
result.currency # => "USD"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Network Fields
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
result.isp # => "Google LLC"
|
|
231
|
+
result.org # => "Google Public DNS"
|
|
232
|
+
result.as_number # => "AS15169 Google LLC"
|
|
233
|
+
result.as_name # => "GOOGLE"
|
|
234
|
+
result.reverse # => "dns.google"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Connection Fields
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
result.mobile # => false
|
|
241
|
+
result.proxy # => false
|
|
242
|
+
result.hosting # => true
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Helper Methods
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
result.success? # => true (status == 'success')
|
|
249
|
+
result.failed? # => false (status == 'fail')
|
|
250
|
+
|
|
251
|
+
result.mobile? # => false
|
|
252
|
+
result.proxy? # => false
|
|
253
|
+
result.hosting? # => true
|
|
254
|
+
|
|
255
|
+
result.coordinates # => [39.03, -77.5]
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Error Handling
|
|
261
|
+
|
|
262
|
+
The API returns errors in two ways:
|
|
263
|
+
|
|
264
|
+
**HTTP errors** (rate limiting):
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
response = IpApi.query( '8.8.8.8' )
|
|
268
|
+
|
|
269
|
+
unless response.success?
|
|
270
|
+
error = response.result
|
|
271
|
+
puts error.error_type # => :rate_limit_error
|
|
272
|
+
puts error.error_description # => "The rate limit has been exceeded..."
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**API errors** (invalid query, private IP):
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
response = IpApi.query( '192.168.1.1' )
|
|
280
|
+
|
|
281
|
+
if response.success?
|
|
282
|
+
result = response.result
|
|
283
|
+
if result.failed?
|
|
284
|
+
puts result.message # => "private range"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Common API error messages:
|
|
290
|
+
|
|
291
|
+
| Message | Description |
|
|
292
|
+
|---------|-------------|
|
|
293
|
+
| `private range` | IP is in a private network range |
|
|
294
|
+
| `reserved range` | IP is in a reserved range |
|
|
295
|
+
| `invalid query` | Invalid IP or domain |
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Connections
|
|
300
|
+
|
|
301
|
+
The gem uses Faraday for HTTP requests. To customize the connection:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
connection = Faraday.new do | faraday |
|
|
305
|
+
faraday.response :logger
|
|
306
|
+
faraday.adapter :net_http
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
IpApi.connection connection
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Or pass it directly to a request:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
request = IpApi::QueryRequest.new(
|
|
316
|
+
api_key: ENV[ 'IPAPI_API_KEY' ],
|
|
317
|
+
connection: connection
|
|
318
|
+
)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## License
|
|
324
|
+
|
|
325
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/bin/ipapi
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'ip_api'
|
|
4
|
+
|
|
5
|
+
def usage
|
|
6
|
+
puts "Usage: ipapi [options] [ip_or_domain]"
|
|
7
|
+
puts
|
|
8
|
+
puts "Look up geolocation and network information for an IP address or domain."
|
|
9
|
+
puts "If no IP/domain is provided, looks up the current machine's public IP."
|
|
10
|
+
puts
|
|
11
|
+
puts "Options:"
|
|
12
|
+
puts " -f, --fields <list> Comma-separated fields to return"
|
|
13
|
+
puts " -l, --lang <code> Language for localized fields (en, de, es, fr, ja, ru, pt-BR, zh-CN)"
|
|
14
|
+
puts " -j, --json Output raw JSON"
|
|
15
|
+
puts " -h, --help Show this help message"
|
|
16
|
+
puts
|
|
17
|
+
puts "Environment:"
|
|
18
|
+
puts " IPAPI_API_KEY Your IP-API Pro key (optional, enables HTTPS and unlimited requests)"
|
|
19
|
+
puts
|
|
20
|
+
puts "Examples:"
|
|
21
|
+
puts " ipapi 8.8.8.8"
|
|
22
|
+
puts " ipapi google.com"
|
|
23
|
+
puts " ipapi # Look up your own IP"
|
|
24
|
+
puts " ipapi -f country,city 8.8.8.8"
|
|
25
|
+
puts " ipapi -l de 8.8.8.8 # German localization"
|
|
26
|
+
puts " ipapi -j 8.8.8.8 # Raw JSON output"
|
|
27
|
+
puts
|
|
28
|
+
puts "Available fields:"
|
|
29
|
+
puts " status, message, query, continent, continent_code, country, country_code,"
|
|
30
|
+
puts " region, region_name, city, district, zip, lat, lon, timezone, offset,"
|
|
31
|
+
puts " currency, isp, org, as, as_name, reverse, mobile, proxy, hosting"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def parse_options( args )
|
|
35
|
+
options = {}
|
|
36
|
+
query = nil
|
|
37
|
+
|
|
38
|
+
while args.any?
|
|
39
|
+
arg = args.shift
|
|
40
|
+
case arg
|
|
41
|
+
when '-f', '--fields'
|
|
42
|
+
fields_str = args.shift
|
|
43
|
+
if fields_str
|
|
44
|
+
options[ :fields ] = fields_str.split( ',' ).map { | f | f.strip.to_sym }
|
|
45
|
+
end
|
|
46
|
+
when '-l', '--lang'
|
|
47
|
+
lang = args.shift
|
|
48
|
+
options[ :lang ] = lang.to_sym if lang
|
|
49
|
+
when '-j', '--json'
|
|
50
|
+
options[ :json ] = true
|
|
51
|
+
when '-h', '--help'
|
|
52
|
+
usage
|
|
53
|
+
exit 0
|
|
54
|
+
when /^-/
|
|
55
|
+
$stderr.puts "Unknown option: #{ arg }"
|
|
56
|
+
$stderr.puts "Run 'ipapi --help' for usage"
|
|
57
|
+
exit 1
|
|
58
|
+
else
|
|
59
|
+
query = arg
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
[ query, options ]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def format_result( result )
|
|
67
|
+
output = []
|
|
68
|
+
|
|
69
|
+
output << [ 'Query', result.query ] if result.query
|
|
70
|
+
output << [ 'Status', result.status ] if result.status
|
|
71
|
+
|
|
72
|
+
if result.failed?
|
|
73
|
+
output << [ 'Message', result.message ] if result.message
|
|
74
|
+
else
|
|
75
|
+
# Location
|
|
76
|
+
output << [ 'Continent', "#{ result.continent } (#{ result.continent_code })" ] if result.continent
|
|
77
|
+
output << [ 'Country', "#{ result.country } (#{ result.country_code })" ] if result.country
|
|
78
|
+
output << [ 'Region', "#{ result.region_name } (#{ result.region })" ] if result.region_name
|
|
79
|
+
output << [ 'City', result.city ] if result.city
|
|
80
|
+
output << [ 'District', result.district ] if result.district
|
|
81
|
+
output << [ 'ZIP', result.zip ] if result.zip
|
|
82
|
+
output << [ 'Coordinates', "#{ result.lat }, #{ result.lon }" ] if result.lat && result.lon
|
|
83
|
+
output << [ 'Timezone', result.timezone ] if result.timezone
|
|
84
|
+
output << [ 'UTC Offset', result.offset ] if result.offset
|
|
85
|
+
output << [ 'Currency', result.currency ] if result.currency
|
|
86
|
+
|
|
87
|
+
# Network
|
|
88
|
+
output << [ 'ISP', result.isp ] if result.isp
|
|
89
|
+
output << [ 'Organization', result.org ] if result.org
|
|
90
|
+
output << [ 'AS', result.as_number ] if result.as_number
|
|
91
|
+
output << [ 'AS Name', result.as_name ] if result.as_name
|
|
92
|
+
output << [ 'Reverse DNS', result.reverse ] if result.reverse
|
|
93
|
+
|
|
94
|
+
# Flags
|
|
95
|
+
flags = []
|
|
96
|
+
flags << 'Mobile' if result.mobile?
|
|
97
|
+
flags << 'Proxy/VPN' if result.proxy?
|
|
98
|
+
flags << 'Hosting' if result.hosting?
|
|
99
|
+
output << [ 'Flags', flags.join( ', ' ) ] unless flags.empty?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
max_label = output.map { | label, _ | label.length }.max || 0
|
|
103
|
+
output.each do | label, value |
|
|
104
|
+
puts "#{ label.ljust( max_label ) } #{ value }"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Main
|
|
109
|
+
query, options = parse_options( ARGV.dup )
|
|
110
|
+
|
|
111
|
+
# Set API key from environment
|
|
112
|
+
IpApi.api_key ENV[ 'IPAPI_API_KEY' ] if ENV[ 'IPAPI_API_KEY' ]
|
|
113
|
+
|
|
114
|
+
# Build request options
|
|
115
|
+
request_options = nil
|
|
116
|
+
if options[ :fields ] || options[ :lang ]
|
|
117
|
+
request_options = IpApi::QueryOptions.build do
|
|
118
|
+
fields options[ :fields ] if options[ :fields ]
|
|
119
|
+
lang options[ :lang ] if options[ :lang ]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Make request
|
|
124
|
+
response = IpApi.query( query, request_options )
|
|
125
|
+
|
|
126
|
+
unless response.success?
|
|
127
|
+
$stderr.puts "HTTP Error: #{ response.result.error_description }"
|
|
128
|
+
exit 1
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
result = response.result
|
|
132
|
+
|
|
133
|
+
if options[ :json ]
|
|
134
|
+
require 'json'
|
|
135
|
+
puts JSON.pretty_generate( result.to_h )
|
|
136
|
+
else
|
|
137
|
+
format_result( result )
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
exit( result.success? ? 0 : 1 )
|
data/ipapi-rb.gemspec
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require_relative 'lib/ip_api/version'
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do | spec |
|
|
4
|
+
|
|
5
|
+
spec.name = 'ipapi-rb'
|
|
6
|
+
spec.version = IpApi::VERSION
|
|
7
|
+
spec.authors = [ 'Kristoph Cichocki-Romanov' ]
|
|
8
|
+
spec.email = [ 'rubygems.org@kristoph.net' ]
|
|
9
|
+
|
|
10
|
+
spec.summary =
|
|
11
|
+
"The IpApi gem implements a lightweight interface to the IP-API.com geolocation " \
|
|
12
|
+
"API for IP address and domain lookups."
|
|
13
|
+
spec.description =
|
|
14
|
+
"The IpApi gem implements a lightweight interface to the IP-API.com geolocation " \
|
|
15
|
+
"API. IP-API provides location and network information for any IP address or domain " \
|
|
16
|
+
"name in JSON format.\n" \
|
|
17
|
+
"\n" \
|
|
18
|
+
"This gem supports both the free endpoint (rate-limited, HTTP only) and the Pro " \
|
|
19
|
+
"endpoint (unlimited, HTTPS) with automatic endpoint selection based on API key " \
|
|
20
|
+
"presence."
|
|
21
|
+
spec.license = 'MIT'
|
|
22
|
+
spec.homepage = 'https://github.com/EndlessInternational/ipapi-rb'
|
|
23
|
+
spec.metadata = {
|
|
24
|
+
'source_code_uri' => 'https://github.com/EndlessInternational/ipapi-rb',
|
|
25
|
+
'bug_tracker_uri' => 'https://github.com/EndlessInternational/ipapi-rb/issues',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
spec.required_ruby_version = '>= 3.0'
|
|
29
|
+
spec.files = Dir[ "lib/**/*.rb", "bin/ipapi", "LICENSE", "README.md", "ipapi-rb.gemspec" ]
|
|
30
|
+
spec.bindir = 'bin'
|
|
31
|
+
spec.executables = [ 'ipapi' ]
|
|
32
|
+
spec.require_paths = [ "lib" ]
|
|
33
|
+
|
|
34
|
+
spec.add_runtime_dependency 'faraday', '~> 2'
|
|
35
|
+
spec.add_runtime_dependency 'dynamicschema', '~> 2'
|
|
36
|
+
|
|
37
|
+
spec.add_development_dependency 'minitest', '~> 5.25'
|
|
38
|
+
spec.add_development_dependency 'debug', '~> 1.9'
|
|
39
|
+
spec.add_development_dependency 'vcr', '~> 6.3'
|
|
40
|
+
|
|
41
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module IpApi
|
|
2
|
+
class ErrorResult
|
|
3
|
+
|
|
4
|
+
attr_reader :error_type, :error_description
|
|
5
|
+
|
|
6
|
+
def initialize( status_code, attributes = nil )
|
|
7
|
+
@status_code = status_code
|
|
8
|
+
@error_type, @error_description = status_code_to_error( status_code )
|
|
9
|
+
if attributes&.respond_to?( :[] )
|
|
10
|
+
@error_description = attributes[ :message ] if attributes[ :message ]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def success?
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# ip-api returns HTTP 200 for most errors with status: "fail" in the body
|
|
21
|
+
# only rate limiting returns HTTP 429
|
|
22
|
+
def status_code_to_error( status_code )
|
|
23
|
+
case status_code
|
|
24
|
+
when 200
|
|
25
|
+
[ :unexpected_error,
|
|
26
|
+
"The response was successful but it did not include a valid payload." ]
|
|
27
|
+
when 429
|
|
28
|
+
[ :rate_limit_error,
|
|
29
|
+
"The rate limit has been exceeded. Free tier allows 45 requests per minute." ]
|
|
30
|
+
when 500..599
|
|
31
|
+
[ :server_error,
|
|
32
|
+
"The IP-API server encountered an error while processing the request." ]
|
|
33
|
+
else
|
|
34
|
+
[ :http_error,
|
|
35
|
+
"The IP-API service returned HTTP status code: '#{ status_code }'." ]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module IpApi
|
|
2
|
+
module ModuleMethods
|
|
3
|
+
|
|
4
|
+
def connection( connection = nil )
|
|
5
|
+
@connection = connection if connection
|
|
6
|
+
@connection ||= Faraday.new { | builder | builder.adapter Faraday.default_adapter }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def api_key( api_key = nil )
|
|
10
|
+
@api_key = api_key || @api_key
|
|
11
|
+
@api_key
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def query( query = nil, options = nil, &block )
|
|
15
|
+
IpApi::QueryRequest.new.submit( query, options, &block )
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module IpApi
|
|
2
|
+
class QueryOptions
|
|
3
|
+
include DynamicSchema::Definable
|
|
4
|
+
include Helpers
|
|
5
|
+
|
|
6
|
+
LANGUAGES = [ :en, :de, :es, :'pt-BR', :fr, :ja, :'zh-CN', :ru ]
|
|
7
|
+
|
|
8
|
+
FIELDS = [
|
|
9
|
+
:status, :message, :query,
|
|
10
|
+
:continent, :continent_code, :country, :country_code, :region, :region_name,
|
|
11
|
+
:city, :district, :zip, :lat, :lon, :timezone, :offset, :currency,
|
|
12
|
+
:isp, :org, :as, :as_name, :reverse,
|
|
13
|
+
:mobile, :proxy, :hosting
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
schema do
|
|
17
|
+
fields Symbol, array: true, in: FIELDS
|
|
18
|
+
lang Symbol, in: LANGUAGES
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.build( options = nil, &block )
|
|
22
|
+
new( api_options: builder.build( options, &block ) )
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.build!( options = nil, &block )
|
|
26
|
+
new( api_options: builder.build!( options, &block ) )
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize( options = {}, api_options: nil )
|
|
30
|
+
@options = self.class.builder.build( options || {} )
|
|
31
|
+
@options = api_options.merge( @options ) if api_options
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
@options.to_h
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_params
|
|
39
|
+
params = {}
|
|
40
|
+
hash = to_h
|
|
41
|
+
|
|
42
|
+
if hash[ :fields ] && !hash[ :fields ].empty?
|
|
43
|
+
params[ :fields ] = hash[ :fields ].map { | f | _snake_to_camel( f ) }.join( ',' )
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if hash[ :lang ]
|
|
47
|
+
params[ :lang ] = hash[ :lang ].to_s
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
params
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def _snake_to_camel( value )
|
|
56
|
+
value.to_s.gsub( /_([a-z])/ ) { $1.upcase }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module IpApi
|
|
2
|
+
class QueryRequest < Request
|
|
3
|
+
|
|
4
|
+
def submit( query = nil, options = nil, &block )
|
|
5
|
+
if options
|
|
6
|
+
options = options.is_a?( QueryOptions ) ? options : QueryOptions.build!( options.to_h )
|
|
7
|
+
params = options.to_params
|
|
8
|
+
else
|
|
9
|
+
params = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
path = query ? "/json/#{ query }" : "/json/"
|
|
13
|
+
response = get( "#{ base_uri }#{ path }", params, &block )
|
|
14
|
+
attributes = ( JSON.parse( response.body, symbolize_names: true ) rescue nil )
|
|
15
|
+
|
|
16
|
+
result = if response.success? && attributes
|
|
17
|
+
QueryResult.new( attributes )
|
|
18
|
+
else
|
|
19
|
+
ErrorResult.new( response.status, attributes )
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
ResponseMethods.install( response, result )
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
|
|
3
|
+
module IpApi
|
|
4
|
+
|
|
5
|
+
QueryResultSchema = DynamicSchema::Struct.define do
|
|
6
|
+
status String
|
|
7
|
+
message String
|
|
8
|
+
query String
|
|
9
|
+
|
|
10
|
+
continent String
|
|
11
|
+
continent_code String, as: :continentCode
|
|
12
|
+
country String
|
|
13
|
+
country_code String, as: :countryCode
|
|
14
|
+
region String
|
|
15
|
+
region_name String, as: :regionName
|
|
16
|
+
city String
|
|
17
|
+
district String
|
|
18
|
+
zip String
|
|
19
|
+
lat Float
|
|
20
|
+
lon Float
|
|
21
|
+
timezone String
|
|
22
|
+
offset Integer
|
|
23
|
+
currency String
|
|
24
|
+
|
|
25
|
+
isp String
|
|
26
|
+
org String
|
|
27
|
+
as_number String, as: :as
|
|
28
|
+
as_name String, as: :asname
|
|
29
|
+
reverse String
|
|
30
|
+
|
|
31
|
+
mobile [ TrueClass, FalseClass ]
|
|
32
|
+
proxy [ TrueClass, FalseClass ]
|
|
33
|
+
hosting [ TrueClass, FalseClass ]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class QueryResult < QueryResultSchema
|
|
37
|
+
|
|
38
|
+
def success?
|
|
39
|
+
status == 'success'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def failed?
|
|
43
|
+
status == 'fail'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def mobile?
|
|
47
|
+
mobile == true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def proxy?
|
|
51
|
+
proxy == true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def hosting?
|
|
55
|
+
hosting == true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def coordinates
|
|
59
|
+
[ lat, lon ] if lat && lon
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module IpApi
|
|
2
|
+
class Request
|
|
3
|
+
|
|
4
|
+
FREE_BASE_URI = 'http://ip-api.com'.freeze
|
|
5
|
+
PRO_BASE_URI = 'https://pro.ip-api.com'.freeze
|
|
6
|
+
|
|
7
|
+
def initialize( connection: nil, api_key: nil )
|
|
8
|
+
@connection = connection || IpApi.connection
|
|
9
|
+
@api_key = api_key || IpApi.api_key
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def pro?
|
|
13
|
+
!@api_key.nil? && !@api_key.empty?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def base_uri
|
|
17
|
+
pro? ? PRO_BASE_URI : FREE_BASE_URI
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
protected
|
|
21
|
+
|
|
22
|
+
def get( uri, params = {}, &block )
|
|
23
|
+
params[ :key ] = @api_key if pro?
|
|
24
|
+
|
|
25
|
+
@connection.get( uri ) do | request |
|
|
26
|
+
request.params = params unless params.empty?
|
|
27
|
+
block.call( request ) if block
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/ip_api.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'uri'
|
|
3
|
+
|
|
4
|
+
require 'faraday'
|
|
5
|
+
require 'dynamic_schema'
|
|
6
|
+
|
|
7
|
+
require_relative 'ip_api/version'
|
|
8
|
+
|
|
9
|
+
require_relative 'ip_api/helpers'
|
|
10
|
+
require_relative 'ip_api/error_result'
|
|
11
|
+
require_relative 'ip_api/request'
|
|
12
|
+
require_relative 'ip_api/response_methods'
|
|
13
|
+
|
|
14
|
+
require_relative 'ip_api/query_options'
|
|
15
|
+
require_relative 'ip_api/query_result'
|
|
16
|
+
require_relative 'ip_api/query_request'
|
|
17
|
+
|
|
18
|
+
require_relative 'ip_api/module_methods'
|
|
19
|
+
|
|
20
|
+
module IpApi
|
|
21
|
+
extend ModuleMethods
|
|
22
|
+
end
|
data/lib/ipapi-rb.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative 'ip_api'
|
metadata
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ipapi-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kristoph Cichocki-Romanov
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: dynamicschema
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.25'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.25'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: debug
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.9'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.9'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: vcr
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '6.3'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '6.3'
|
|
82
|
+
description: |-
|
|
83
|
+
The IpApi gem implements a lightweight interface to the IP-API.com geolocation API. IP-API provides location and network information for any IP address or domain name in JSON format.
|
|
84
|
+
|
|
85
|
+
This gem supports both the free endpoint (rate-limited, HTTP only) and the Pro endpoint (unlimited, HTTPS) with automatic endpoint selection based on API key presence.
|
|
86
|
+
email:
|
|
87
|
+
- rubygems.org@kristoph.net
|
|
88
|
+
executables:
|
|
89
|
+
- ipapi
|
|
90
|
+
extensions: []
|
|
91
|
+
extra_rdoc_files: []
|
|
92
|
+
files:
|
|
93
|
+
- README.md
|
|
94
|
+
- bin/ipapi
|
|
95
|
+
- ipapi-rb.gemspec
|
|
96
|
+
- lib/ip_api.rb
|
|
97
|
+
- lib/ip_api/error_result.rb
|
|
98
|
+
- lib/ip_api/helpers.rb
|
|
99
|
+
- lib/ip_api/module_methods.rb
|
|
100
|
+
- lib/ip_api/query_options.rb
|
|
101
|
+
- lib/ip_api/query_request.rb
|
|
102
|
+
- lib/ip_api/query_result.rb
|
|
103
|
+
- lib/ip_api/request.rb
|
|
104
|
+
- lib/ip_api/response_methods.rb
|
|
105
|
+
- lib/ip_api/version.rb
|
|
106
|
+
- lib/ipapi-rb.rb
|
|
107
|
+
homepage: https://github.com/EndlessInternational/ipapi-rb
|
|
108
|
+
licenses:
|
|
109
|
+
- MIT
|
|
110
|
+
metadata:
|
|
111
|
+
source_code_uri: https://github.com/EndlessInternational/ipapi-rb
|
|
112
|
+
bug_tracker_uri: https://github.com/EndlessInternational/ipapi-rb/issues
|
|
113
|
+
rdoc_options: []
|
|
114
|
+
require_paths:
|
|
115
|
+
- lib
|
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '3.0'
|
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
122
|
+
requirements:
|
|
123
|
+
- - ">="
|
|
124
|
+
- !ruby/object:Gem::Version
|
|
125
|
+
version: '0'
|
|
126
|
+
requirements: []
|
|
127
|
+
rubygems_version: 3.6.7
|
|
128
|
+
specification_version: 4
|
|
129
|
+
summary: The IpApi gem implements a lightweight interface to the IP-API.com geolocation
|
|
130
|
+
API for IP address and domain lookups.
|
|
131
|
+
test_files: []
|