maxmind-geoip2 0.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 +17 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +70 -0
- data/LICENSE-APACHE +202 -0
- data/LICENSE-MIT +17 -0
- data/README.dev.md +4 -0
- data/README.md +326 -0
- data/Rakefile +14 -0
- data/lib/maxmind/geoip2.rb +4 -0
- data/lib/maxmind/geoip2/client.rb +313 -0
- data/lib/maxmind/geoip2/errors.rb +48 -0
- data/lib/maxmind/geoip2/model/abstract.rb +27 -0
- data/lib/maxmind/geoip2/model/anonymous_ip.rb +63 -0
- data/lib/maxmind/geoip2/model/asn.rb +39 -0
- data/lib/maxmind/geoip2/model/city.rb +75 -0
- data/lib/maxmind/geoip2/model/connection_type.rb +32 -0
- data/lib/maxmind/geoip2/model/country.rb +70 -0
- data/lib/maxmind/geoip2/model/domain.rb +32 -0
- data/lib/maxmind/geoip2/model/enterprise.rb +15 -0
- data/lib/maxmind/geoip2/model/insights.rb +14 -0
- data/lib/maxmind/geoip2/model/isp.rb +53 -0
- data/lib/maxmind/geoip2/reader.rb +277 -0
- data/lib/maxmind/geoip2/record/abstract.rb +22 -0
- data/lib/maxmind/geoip2/record/city.rb +38 -0
- data/lib/maxmind/geoip2/record/continent.rb +37 -0
- data/lib/maxmind/geoip2/record/country.rb +54 -0
- data/lib/maxmind/geoip2/record/location.rb +73 -0
- data/lib/maxmind/geoip2/record/maxmind.rb +17 -0
- data/lib/maxmind/geoip2/record/place.rb +28 -0
- data/lib/maxmind/geoip2/record/postal.rb +30 -0
- data/lib/maxmind/geoip2/record/represented_country.rb +23 -0
- data/lib/maxmind/geoip2/record/subdivision.rb +48 -0
- data/lib/maxmind/geoip2/record/traits.rb +200 -0
- data/maxmind-geoip2.gemspec +25 -0
- data/test/test_client.rb +424 -0
- data/test/test_model_country.rb +80 -0
- data/test/test_model_names.rb +47 -0
- data/test/test_reader.rb +459 -0
- metadata +116 -0
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rubocop/rake_task'
|
5
|
+
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
t.libs << 'test'
|
8
|
+
end
|
9
|
+
|
10
|
+
RuboCop::RakeTask.new
|
11
|
+
|
12
|
+
desc 'Run tests and RuboCop'
|
13
|
+
task default: :test
|
14
|
+
task default: :rubocop
|
@@ -0,0 +1,313 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'http'
|
4
|
+
require 'json'
|
5
|
+
require 'maxmind/geoip2/errors'
|
6
|
+
require 'maxmind/geoip2/model/city'
|
7
|
+
require 'maxmind/geoip2/model/country'
|
8
|
+
require 'maxmind/geoip2/model/insights'
|
9
|
+
|
10
|
+
module MaxMind::GeoIP2
|
11
|
+
# This class provides a client API for all the
|
12
|
+
# {https://dev.maxmind.com/geoip/geoip2/web-services/ GeoIP2 Precision web
|
13
|
+
# services}. The services are Country, City, and Insights. Each service
|
14
|
+
# returns a different set of data about an IP address, with Country returning
|
15
|
+
# the least data and Insights the most.
|
16
|
+
#
|
17
|
+
# Each web service is represented by a different model class, and these model
|
18
|
+
# classes in turn contain multiple record classes. The record classes have
|
19
|
+
# attributes which contain data about the IP address.
|
20
|
+
#
|
21
|
+
# If the web service does not return a particular piece of data for an IP
|
22
|
+
# address, the associated attribute is not populated.
|
23
|
+
#
|
24
|
+
# The web service may not return any information for an entire record, in
|
25
|
+
# which case all of the attributes for that record class will be empty.
|
26
|
+
#
|
27
|
+
# == Usage
|
28
|
+
#
|
29
|
+
# The basic API for this class is the same for all of the web service end
|
30
|
+
# points. First you create a web service client object with your MaxMind
|
31
|
+
# account ID and license key, then you call the method corresponding to a
|
32
|
+
# specific end point, passing it the IP address you want to look up.
|
33
|
+
#
|
34
|
+
# If the request succeeds, the method call will return a model class for the
|
35
|
+
# service you called. This model in turn contains multiple record classes,
|
36
|
+
# each of which represents part of the data returned by the web service.
|
37
|
+
#
|
38
|
+
# If the request fails, the client class throws an exception.
|
39
|
+
#
|
40
|
+
# == Example
|
41
|
+
#
|
42
|
+
# require 'maxmind/geoip2'
|
43
|
+
#
|
44
|
+
# client = MaxMind::GeoIP2::Client.new(
|
45
|
+
# account_id: 42,
|
46
|
+
# license_key: 'abcdef123456',
|
47
|
+
# )
|
48
|
+
#
|
49
|
+
# # Replace 'city' with the method corresponding to the web service you
|
50
|
+
# # are using, e.g., 'country', 'insights'.
|
51
|
+
# record = client.city('128.101.101.101')
|
52
|
+
#
|
53
|
+
# puts record.country.iso_code
|
54
|
+
class Client
|
55
|
+
# rubocop:disable Metrics/ParameterLists
|
56
|
+
|
57
|
+
# Create a Client that may be used to query a GeoIP2 Precision web service.
|
58
|
+
#
|
59
|
+
# Once created, the Client is safe to use for lookups from multiple
|
60
|
+
# threads.
|
61
|
+
#
|
62
|
+
# @param account_id [Integer] your MaxMind account ID.
|
63
|
+
#
|
64
|
+
# @param license_key [String] your MaxMind license key.
|
65
|
+
#
|
66
|
+
# @param locales [Array<String>] a list of locale codes to use in the name
|
67
|
+
# property from most preferred to least preferred.
|
68
|
+
#
|
69
|
+
# @param host [String] the host to use when querying the web service.
|
70
|
+
#
|
71
|
+
# @param timeout [Integer] the number of seconds to wait for a request
|
72
|
+
# before timing out. If 0, no timeout is set.
|
73
|
+
#
|
74
|
+
# @param proxy_address [String] proxy address to use, if any.
|
75
|
+
#
|
76
|
+
# @param proxy_port [Integer] proxy port to use, if any.
|
77
|
+
#
|
78
|
+
# @param proxy_username [String] proxy username to use, if any.
|
79
|
+
#
|
80
|
+
# @param proxy_password [String] proxy password to use, if any.
|
81
|
+
def initialize(
|
82
|
+
account_id:,
|
83
|
+
license_key:,
|
84
|
+
locales: ['en'],
|
85
|
+
host: 'geoip.maxmind.com',
|
86
|
+
timeout: 0,
|
87
|
+
proxy_address: '',
|
88
|
+
proxy_port: 0,
|
89
|
+
proxy_username: '',
|
90
|
+
proxy_password: ''
|
91
|
+
)
|
92
|
+
@account_id = account_id
|
93
|
+
@license_key = license_key
|
94
|
+
@locales = locales
|
95
|
+
@host = host
|
96
|
+
@timeout = timeout
|
97
|
+
@proxy_address = proxy_address
|
98
|
+
@proxy_port = proxy_port
|
99
|
+
@proxy_username = proxy_username
|
100
|
+
@proxy_password = proxy_password
|
101
|
+
end
|
102
|
+
# rubocop:enable Metrics/ParameterLists
|
103
|
+
|
104
|
+
# This method calls the GeoIP2 Precision City web service.
|
105
|
+
#
|
106
|
+
# @param ip_address [String] IPv4 or IPv6 address as a string. If no
|
107
|
+
# address is provided, the address that the web service is called from is
|
108
|
+
# used.
|
109
|
+
#
|
110
|
+
# @raise [HTTP::Error] if there was an error performing the HTTP request,
|
111
|
+
# such as an error connecting.
|
112
|
+
#
|
113
|
+
# @raise [JSON::ParserError] if there was invalid JSON in the response.
|
114
|
+
#
|
115
|
+
# @raise [HTTPError] if there was a problem with the HTTP response, such as
|
116
|
+
# an unexpected HTTP status code.
|
117
|
+
#
|
118
|
+
# @raise [AddressInvalidError] if the web service believes the IP address
|
119
|
+
# to be invalid or missing.
|
120
|
+
#
|
121
|
+
# @raise [AddressNotFoundError] if the IP address was not found.
|
122
|
+
#
|
123
|
+
# @raise [AddressReservedError] if the IP address is reserved.
|
124
|
+
#
|
125
|
+
# @raise [AuthenticationError] if there was a problem authenticating to the
|
126
|
+
# web service, such as an invalid or missing license key.
|
127
|
+
#
|
128
|
+
# @raise [InsufficientFundsError] if your account is out of credit.
|
129
|
+
#
|
130
|
+
# @raise [PermissionRequiredError] if your account does not have permission
|
131
|
+
# to use the web service.
|
132
|
+
#
|
133
|
+
# @raise [InvalidRequestError] if the web service responded with an error
|
134
|
+
# and there is no more specific error to raise.
|
135
|
+
#
|
136
|
+
# @return [MaxMind::GeoIP2::Model::City]
|
137
|
+
def city(ip_address = 'me')
|
138
|
+
response_for('city', MaxMind::GeoIP2::Model::City, ip_address)
|
139
|
+
end
|
140
|
+
|
141
|
+
# This method calls the GeoIP2 Precision Country web service.
|
142
|
+
#
|
143
|
+
# @param ip_address [String] IPv4 or IPv6 address as a string. If no
|
144
|
+
# address is provided, the address that the web service is called from is
|
145
|
+
# used.
|
146
|
+
#
|
147
|
+
# @raise [HTTP::Error] if there was an error performing the HTTP request,
|
148
|
+
# such as an error connecting.
|
149
|
+
#
|
150
|
+
# @raise [JSON::ParserError] if there was invalid JSON in the response.
|
151
|
+
#
|
152
|
+
# @raise [HTTPError] if there was a problem with the HTTP response, such as
|
153
|
+
# an unexpected HTTP status code.
|
154
|
+
#
|
155
|
+
# @raise [AddressInvalidError] if the web service believes the IP address
|
156
|
+
# to be invalid or missing.
|
157
|
+
#
|
158
|
+
# @raise [AddressNotFoundError] if the IP address was not found.
|
159
|
+
#
|
160
|
+
# @raise [AddressReservedError] if the IP address is reserved.
|
161
|
+
#
|
162
|
+
# @raise [AuthenticationError] if there was a problem authenticating to the
|
163
|
+
# web service, such as an invalid or missing license key.
|
164
|
+
#
|
165
|
+
# @raise [InsufficientFundsError] if your account is out of credit.
|
166
|
+
#
|
167
|
+
# @raise [PermissionRequiredError] if your account does not have permission
|
168
|
+
# to use the web service.
|
169
|
+
#
|
170
|
+
# @raise [InvalidRequestError] if the web service responded with an error
|
171
|
+
# and there is no more specific error to raise.
|
172
|
+
#
|
173
|
+
# @return [MaxMind::GeoIP2::Model::Country]
|
174
|
+
def country(ip_address = 'me')
|
175
|
+
response_for('country', MaxMind::GeoIP2::Model::Country, ip_address)
|
176
|
+
end
|
177
|
+
|
178
|
+
# This method calls the GeoIP2 Precision Insights web service.
|
179
|
+
#
|
180
|
+
# @param ip_address [String] IPv4 or IPv6 address as a string. If no
|
181
|
+
# address is provided, the address that the web service is called from is
|
182
|
+
# used.
|
183
|
+
#
|
184
|
+
# @raise [HTTP::Error] if there was an error performing the HTTP request,
|
185
|
+
# such as an error connecting.
|
186
|
+
#
|
187
|
+
# @raise [JSON::ParserError] if there was invalid JSON in the response.
|
188
|
+
#
|
189
|
+
# @raise [HTTPError] if there was a problem with the HTTP response, such as
|
190
|
+
# an unexpected HTTP status code.
|
191
|
+
#
|
192
|
+
# @raise [AddressInvalidError] if the web service believes the IP address
|
193
|
+
# to be invalid or missing.
|
194
|
+
#
|
195
|
+
# @raise [AddressNotFoundError] if the IP address was not found.
|
196
|
+
#
|
197
|
+
# @raise [AddressReservedError] if the IP address is reserved.
|
198
|
+
#
|
199
|
+
# @raise [AuthenticationError] if there was a problem authenticating to the
|
200
|
+
# web service, such as an invalid or missing license key.
|
201
|
+
#
|
202
|
+
# @raise [InsufficientFundsError] if your account is out of credit.
|
203
|
+
#
|
204
|
+
# @raise [PermissionRequiredError] if your account does not have permission
|
205
|
+
# to use the web service.
|
206
|
+
#
|
207
|
+
# @raise [InvalidRequestError] if the web service responded with an error
|
208
|
+
# and there is no more specific error to raise.
|
209
|
+
#
|
210
|
+
# @return [MaxMind::GeoIP2::Model::Insights]
|
211
|
+
def insights(ip_address = 'me')
|
212
|
+
response_for('insights', MaxMind::GeoIP2::Model::Insights, ip_address)
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
def response_for(endpoint, model_class, ip_address)
|
218
|
+
record = get(endpoint, ip_address)
|
219
|
+
|
220
|
+
model_class.new(record, @locales)
|
221
|
+
end
|
222
|
+
|
223
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
224
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
225
|
+
def get(endpoint, ip_address)
|
226
|
+
url = 'https://' + @host + '/geoip/v2.1/' + endpoint + '/' + ip_address
|
227
|
+
|
228
|
+
headers = HTTP.basic_auth(user: @account_id, pass: @license_key)
|
229
|
+
.headers(
|
230
|
+
accept: 'application/json',
|
231
|
+
user_agent: 'MaxMind-GeoIP2-ruby',
|
232
|
+
)
|
233
|
+
timeout = @timeout > 0 ? headers.timeout(@timeout) : headers
|
234
|
+
|
235
|
+
proxy = timeout
|
236
|
+
if @proxy_address != ''
|
237
|
+
opts = {}
|
238
|
+
opts[:proxy_port] = @proxy_port if @proxy_port != 0
|
239
|
+
opts[:proxy_username] = @proxy_username if @proxy_username != ''
|
240
|
+
opts[:proxy_password] = @proxy_password if @proxy_password != ''
|
241
|
+
proxy = timeout.via(@proxy_address, opts)
|
242
|
+
end
|
243
|
+
|
244
|
+
response = proxy.get(url)
|
245
|
+
|
246
|
+
body = response.to_s
|
247
|
+
is_json = response.headers[:content_type]&.include?('json')
|
248
|
+
|
249
|
+
if response.status.client_error?
|
250
|
+
return handle_client_error(endpoint, response.code, body, is_json)
|
251
|
+
end
|
252
|
+
|
253
|
+
if response.status.server_error?
|
254
|
+
raise HTTPError,
|
255
|
+
"Received server error response (#{response.code}) for #{endpoint} with body #{body}"
|
256
|
+
end
|
257
|
+
|
258
|
+
if response.code != 200
|
259
|
+
raise HTTPError,
|
260
|
+
"Received unexpected response (#{response.code}) for #{endpoint} with body #{body}"
|
261
|
+
end
|
262
|
+
|
263
|
+
handle_success(endpoint, body, is_json)
|
264
|
+
end
|
265
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
266
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
267
|
+
|
268
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
269
|
+
def handle_client_error(endpoint, status, body, is_json)
|
270
|
+
if !is_json
|
271
|
+
raise HTTPError,
|
272
|
+
"Received client error response (#{status}) for #{endpoint} but it is not JSON: #{body}"
|
273
|
+
end
|
274
|
+
|
275
|
+
error = JSON.parse(body)
|
276
|
+
|
277
|
+
if !error.key?('code') || !error.key?('error')
|
278
|
+
raise HTTPError,
|
279
|
+
"Received client error response (#{status}) that is JSON but does not specify code or error keys: #{body}"
|
280
|
+
end
|
281
|
+
|
282
|
+
case error['code']
|
283
|
+
when 'IP_ADDRESS_INVALID', 'IP_ADDRESS_REQUIRED'
|
284
|
+
raise AddressInvalidError, error['error']
|
285
|
+
when 'IP_ADDRESS_NOT_FOUND'
|
286
|
+
raise AddressNotFoundError, error['error']
|
287
|
+
when 'IP_ADDRESS_RESERVED'
|
288
|
+
raise AddressReservedError, error['error']
|
289
|
+
when 'ACCOUNT_ID_REQUIRED',
|
290
|
+
'ACCOUNT_ID_UNKNOWN',
|
291
|
+
'AUTHORIZATION_INVALID',
|
292
|
+
'LICENSE_KEY_REQUIRED'
|
293
|
+
raise AuthenticationError, error['error']
|
294
|
+
when 'INSUFFICIENT_FUNDS'
|
295
|
+
raise InsufficientFundsError, error['error']
|
296
|
+
when 'PERMISSION_REQUIRED'
|
297
|
+
raise PermissionRequiredError, error['error']
|
298
|
+
else
|
299
|
+
raise InvalidRequestError, error['error']
|
300
|
+
end
|
301
|
+
end
|
302
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
303
|
+
|
304
|
+
def handle_success(endpoint, body, is_json)
|
305
|
+
if !is_json
|
306
|
+
raise HTTPError,
|
307
|
+
"Received a success response for #{endpoint} but it is not JSON: #{body}"
|
308
|
+
end
|
309
|
+
|
310
|
+
JSON.parse(body)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Disable this because I wish to ensure the MaxMind constant is defined,
|
4
|
+
# which apparently must be done without using the compact syntax.
|
5
|
+
#
|
6
|
+
# rubocop:disable Style/ClassAndModuleChildren
|
7
|
+
|
8
|
+
# A module for namespacing purposes.
|
9
|
+
module MaxMind
|
10
|
+
module GeoIP2
|
11
|
+
# An AddressNotFoundError means the IP address was not found in the
|
12
|
+
# database or the web service said the IP address was not found.
|
13
|
+
class AddressNotFoundError < RuntimeError
|
14
|
+
end
|
15
|
+
|
16
|
+
# An HTTPError means there was an unexpected HTTP status or response.
|
17
|
+
class HTTPError < RuntimeError
|
18
|
+
end
|
19
|
+
|
20
|
+
# An AddressInvalidError means the IP address was invalid.
|
21
|
+
class AddressInvalidError < RuntimeError
|
22
|
+
end
|
23
|
+
|
24
|
+
# An AddressReservedError means the IP address is reserved.
|
25
|
+
class AddressReservedError < RuntimeError
|
26
|
+
end
|
27
|
+
|
28
|
+
# An AuthenticationError means there was a problem authenticating to the
|
29
|
+
# web service.
|
30
|
+
class AuthenticationError < RuntimeError
|
31
|
+
end
|
32
|
+
|
33
|
+
# An InsufficientFundsError means the account is out of credits.
|
34
|
+
class InsufficientFundsError < RuntimeError
|
35
|
+
end
|
36
|
+
|
37
|
+
# A PermissionRequiredError means the account does not have permission to
|
38
|
+
# use the requested service.
|
39
|
+
class PermissionRequiredError < RuntimeError
|
40
|
+
end
|
41
|
+
|
42
|
+
# An InvalidRequestError means the web service returned an error and there
|
43
|
+
# is no more specific error class.
|
44
|
+
class InvalidRequestError < RuntimeError
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
# rubocop:enable Style/ClassAndModuleChildren
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ipaddr'
|
4
|
+
|
5
|
+
module MaxMind::GeoIP2::Model
|
6
|
+
# @!visibility private
|
7
|
+
class Abstract
|
8
|
+
def initialize(record)
|
9
|
+
@record = record
|
10
|
+
|
11
|
+
ip = IPAddr.new(record['ip_address']).mask(record['prefix_length'])
|
12
|
+
record['network'] = format('%s/%d', ip.to_s, record['prefix_length'])
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def get(key)
|
18
|
+
if @record.nil? || !@record.key?(key)
|
19
|
+
return false if key.start_with?('is_')
|
20
|
+
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
|
24
|
+
@record[key]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'maxmind/geoip2/model/abstract'
|
4
|
+
|
5
|
+
module MaxMind::GeoIP2::Model
|
6
|
+
# Model class for the Anonymous IP database.
|
7
|
+
class AnonymousIP < Abstract
|
8
|
+
# This is true if the IP address belongs to any sort of anonymous network.
|
9
|
+
#
|
10
|
+
# @return [Boolean]
|
11
|
+
def anonymous?
|
12
|
+
get('is_anonymous')
|
13
|
+
end
|
14
|
+
|
15
|
+
# This is true if the IP address is registered to an anonymous VPN
|
16
|
+
# provider. If a VPN provider does not register subnets under names
|
17
|
+
# associated with them, we will likely only flag their IP ranges using the
|
18
|
+
# hosting_provider? method.
|
19
|
+
#
|
20
|
+
# @return [Boolean]
|
21
|
+
def anonymous_vpn?
|
22
|
+
get('is_anonymous_vpn')
|
23
|
+
end
|
24
|
+
|
25
|
+
# This is true if the IP address belongs to a hosting or VPN provider (see
|
26
|
+
# description of the anonymous_vpn? method).
|
27
|
+
#
|
28
|
+
# @return [Boolean]
|
29
|
+
def hosting_provider?
|
30
|
+
get('is_hosting_provider')
|
31
|
+
end
|
32
|
+
|
33
|
+
# The IP address that the data in the model is for.
|
34
|
+
#
|
35
|
+
# @return [String]
|
36
|
+
def ip_address
|
37
|
+
get('ip_address')
|
38
|
+
end
|
39
|
+
|
40
|
+
# The network in CIDR notation associated with the record. In particular,
|
41
|
+
# this is the largest network where all of the fields besides ip_address
|
42
|
+
# have the same value.
|
43
|
+
#
|
44
|
+
# @return [String]
|
45
|
+
def network
|
46
|
+
get('network')
|
47
|
+
end
|
48
|
+
|
49
|
+
# This is true if the IP address belongs to a public proxy.
|
50
|
+
#
|
51
|
+
# @return [Boolean]
|
52
|
+
def public_proxy?
|
53
|
+
get('is_public_proxy')
|
54
|
+
end
|
55
|
+
|
56
|
+
# This is true if the IP address is a Tor exit node.
|
57
|
+
#
|
58
|
+
# @return [Boolean]
|
59
|
+
def tor_exit_node?
|
60
|
+
get('is_tor_exit_node')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|