maxmind-geoip2 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +17 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.lock +70 -0
  5. data/LICENSE-APACHE +202 -0
  6. data/LICENSE-MIT +17 -0
  7. data/README.dev.md +4 -0
  8. data/README.md +326 -0
  9. data/Rakefile +14 -0
  10. data/lib/maxmind/geoip2.rb +4 -0
  11. data/lib/maxmind/geoip2/client.rb +313 -0
  12. data/lib/maxmind/geoip2/errors.rb +48 -0
  13. data/lib/maxmind/geoip2/model/abstract.rb +27 -0
  14. data/lib/maxmind/geoip2/model/anonymous_ip.rb +63 -0
  15. data/lib/maxmind/geoip2/model/asn.rb +39 -0
  16. data/lib/maxmind/geoip2/model/city.rb +75 -0
  17. data/lib/maxmind/geoip2/model/connection_type.rb +32 -0
  18. data/lib/maxmind/geoip2/model/country.rb +70 -0
  19. data/lib/maxmind/geoip2/model/domain.rb +32 -0
  20. data/lib/maxmind/geoip2/model/enterprise.rb +15 -0
  21. data/lib/maxmind/geoip2/model/insights.rb +14 -0
  22. data/lib/maxmind/geoip2/model/isp.rb +53 -0
  23. data/lib/maxmind/geoip2/reader.rb +277 -0
  24. data/lib/maxmind/geoip2/record/abstract.rb +22 -0
  25. data/lib/maxmind/geoip2/record/city.rb +38 -0
  26. data/lib/maxmind/geoip2/record/continent.rb +37 -0
  27. data/lib/maxmind/geoip2/record/country.rb +54 -0
  28. data/lib/maxmind/geoip2/record/location.rb +73 -0
  29. data/lib/maxmind/geoip2/record/maxmind.rb +17 -0
  30. data/lib/maxmind/geoip2/record/place.rb +28 -0
  31. data/lib/maxmind/geoip2/record/postal.rb +30 -0
  32. data/lib/maxmind/geoip2/record/represented_country.rb +23 -0
  33. data/lib/maxmind/geoip2/record/subdivision.rb +48 -0
  34. data/lib/maxmind/geoip2/record/traits.rb +200 -0
  35. data/maxmind-geoip2.gemspec +25 -0
  36. data/test/test_client.rb +424 -0
  37. data/test/test_model_country.rb +80 -0
  38. data/test/test_model_names.rb +47 -0
  39. data/test/test_reader.rb +459 -0
  40. metadata +116 -0
@@ -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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'maxmind/geoip2/client'
4
+ require 'maxmind/geoip2/reader'
@@ -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