maxmind-geoip2 0.1.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/Gemfile +1 -5
  4. data/README.dev.md +1 -1
  5. data/README.md +82 -8
  6. data/lib/maxmind/geoip2.rb +1 -0
  7. data/lib/maxmind/geoip2/client.rb +333 -0
  8. data/lib/maxmind/geoip2/errors.rb +37 -3
  9. data/lib/maxmind/geoip2/model/abstract.rb +19 -15
  10. data/lib/maxmind/geoip2/model/anonymous_ip.rb +62 -50
  11. data/lib/maxmind/geoip2/model/asn.rb +33 -29
  12. data/lib/maxmind/geoip2/model/city.rb +59 -55
  13. data/lib/maxmind/geoip2/model/connection_type.rb +27 -23
  14. data/lib/maxmind/geoip2/model/country.rb +64 -53
  15. data/lib/maxmind/geoip2/model/domain.rb +27 -23
  16. data/lib/maxmind/geoip2/model/enterprise.rb +13 -9
  17. data/lib/maxmind/geoip2/model/insights.rb +18 -0
  18. data/lib/maxmind/geoip2/model/isp.rb +45 -41
  19. data/lib/maxmind/geoip2/reader.rb +260 -233
  20. data/lib/maxmind/geoip2/record/abstract.rb +17 -13
  21. data/lib/maxmind/geoip2/record/city.rb +33 -29
  22. data/lib/maxmind/geoip2/record/continent.rb +32 -28
  23. data/lib/maxmind/geoip2/record/country.rb +47 -43
  24. data/lib/maxmind/geoip2/record/location.rb +64 -60
  25. data/lib/maxmind/geoip2/record/maxmind.rb +21 -0
  26. data/lib/maxmind/geoip2/record/place.rb +22 -18
  27. data/lib/maxmind/geoip2/record/postal.rb +26 -22
  28. data/lib/maxmind/geoip2/record/represented_country.rb +20 -16
  29. data/lib/maxmind/geoip2/record/subdivision.rb +42 -38
  30. data/lib/maxmind/geoip2/record/traits.rb +204 -191
  31. data/maxmind-geoip2.gemspec +11 -3
  32. data/test/data/bad-data/maxminddb-python/bad-unicode-in-map-key.mmdb +0 -0
  33. data/test/data/source-data/GeoIP2-Anonymous-IP-Test.json +1 -0
  34. data/test/data/source-data/GeoIP2-ISP-Test.json +3 -1
  35. data/test/data/source-data/GeoIP2-Precision-Enterprise-Test.json +87 -0
  36. data/test/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb +0 -0
  37. data/test/data/test-data/GeoIP2-City-Test-Broken-Double-Format.mmdb +0 -0
  38. data/test/data/test-data/GeoIP2-City-Test-Invalid-Node-Count.mmdb +0 -0
  39. data/test/data/test-data/GeoIP2-City-Test.mmdb +0 -0
  40. data/test/data/test-data/GeoIP2-Connection-Type-Test.mmdb +0 -0
  41. data/test/data/test-data/GeoIP2-Country-Test.mmdb +0 -0
  42. data/test/data/test-data/GeoIP2-DensityIncome-Test.mmdb +0 -0
  43. data/test/data/test-data/GeoIP2-Domain-Test.mmdb +0 -0
  44. data/test/data/test-data/GeoIP2-Enterprise-Test.mmdb +0 -0
  45. data/test/data/test-data/GeoIP2-ISP-Test.mmdb +0 -0
  46. data/test/data/test-data/GeoIP2-Precision-Enterprise-Test.mmdb +0 -0
  47. data/test/data/test-data/GeoIP2-Static-IP-Score-Test.mmdb +0 -0
  48. data/test/data/test-data/GeoIP2-User-Count-Test.mmdb +0 -0
  49. data/test/data/test-data/GeoLite2-ASN-Test.mmdb +0 -0
  50. data/test/data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb +0 -0
  51. data/test/data/test-data/MaxMind-DB-string-value-entries.mmdb +0 -0
  52. data/test/data/test-data/MaxMind-DB-test-broken-pointers-24.mmdb +0 -0
  53. data/test/data/test-data/MaxMind-DB-test-broken-search-tree-24.mmdb +0 -0
  54. data/test/data/test-data/MaxMind-DB-test-decoder.mmdb +0 -0
  55. data/test/data/test-data/MaxMind-DB-test-ipv4-24.mmdb +0 -0
  56. data/test/data/test-data/MaxMind-DB-test-ipv4-28.mmdb +0 -0
  57. data/test/data/test-data/MaxMind-DB-test-ipv4-32.mmdb +0 -0
  58. data/test/data/test-data/MaxMind-DB-test-ipv6-24.mmdb +0 -0
  59. data/test/data/test-data/MaxMind-DB-test-ipv6-28.mmdb +0 -0
  60. data/test/data/test-data/MaxMind-DB-test-ipv6-32.mmdb +0 -0
  61. data/test/data/test-data/MaxMind-DB-test-metadata-pointers.mmdb +0 -0
  62. data/test/data/test-data/MaxMind-DB-test-mixed-24.mmdb +0 -0
  63. data/test/data/test-data/MaxMind-DB-test-mixed-28.mmdb +0 -0
  64. data/test/data/test-data/MaxMind-DB-test-mixed-32.mmdb +0 -0
  65. data/test/data/test-data/MaxMind-DB-test-nested.mmdb +0 -0
  66. data/test/data/test-data/MaxMind-DB-test-pointer-decoder.mmdb +0 -0
  67. data/test/data/test-data/write-test-data.pl +68 -18
  68. data/test/test_client.rb +426 -0
  69. data/test/test_model_country.rb +16 -0
  70. data/test/test_reader.rb +59 -0
  71. metadata +113 -10
  72. data/Gemfile.lock +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59a52bbdfb71c9b0ebe0104616db7425e90f166940aa14a04aef3baa60adce7e
4
- data.tar.gz: 51a5b6921967659ac3ee2a874b7625065b71102ce8fc5ee05b0ea7b94f34f233
3
+ metadata.gz: 2825710c836cdc4f085ac2e89f88753aa972124ac307af9669866bb355c61f10
4
+ data.tar.gz: c420943be58fd25b84afe0a51264333c4ef268259da7946b58cd7ef77dd8a46b
5
5
  SHA512:
6
- metadata.gz: 3a75b0ac6a4da68a4c79b9a8b3a61d45c458279189edb5dc98a5366c4dbac567d6834528c6060ad3dbb759316614649b0d8d9bdf571516ca5d33d639dbf7f76c
7
- data.tar.gz: dfa9fb9b9d1f0a10194ee80b4860cd2b83398fbdc86f042a3e9bcf0c90bd4d7621323524167f19467d46faf94bd790283d5ffdb14d8ba66841615dea4d64c37e
6
+ metadata.gz: 30d7e6caeaeded96d60fb981bcc2c4ce518620e973f3dae47433cd1582265fa315706a826c1bc62d29dfce013d5cee78ab1fefff54f51ef858589a54075b24f0
7
+ data.tar.gz: ca89534bc5f0572ad5b69c7c7b92be70089d6e7e600f962d71a38c8c073299d01ed141c6b1b5089cbca9694c6f49b5b8bcb77a45fb1d2ab668cf3f698c40abdb
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 (2021-03-23)
4
+
5
+ * Updated the `MaxMind::GeoIP2::Reader` constructor to support being called
6
+ using keyword arguments. For example, you may now create a `Reader` using
7
+ `MaxMind::GeoIP2::Reader.new(database: 'GeoIP2-Country.mmdb')` instead of
8
+ using positional arguments. This is intended to make it easier to pass in
9
+ optional arguments. Positional argument calling is still supported.
10
+ * Proxy support was fixed. Pull request by Manoj Dayaram. GitHub #30.
11
+
12
+ ## 0.5.0 (2020-09-25)
13
+
14
+ * Added the `residential_proxy?` method to
15
+ `MaxMind::GeoIP2::Model::AnonymousIP` and
16
+ `MaxMind::GeoIP2::Record::Traits` for use with the Anonymous IP database
17
+ and GeoIP2 Precision Insights.
18
+
19
+ ## 0.4.0 (2020-03-06)
20
+
21
+ * HTTP connections are now persistent. There is a new parameter that
22
+ controls the maximum number of connections the client will use.
23
+
24
+ ## 0.3.0 (2020-03-04)
25
+
26
+ * Modules are now always be defined. Previously we used a shorthand syntax
27
+ which meant including individual classes could leave module constants
28
+ undefined.
29
+
30
+ ## 0.2.0 (2020-02-26)
31
+
32
+ * Added support for the GeoIP2 Precision web services: Country, City, and
33
+ Insights.
34
+
3
35
  ## 0.1.0 (2020-02-20)
4
36
 
5
37
  * Added support for the Anonymous IP, ASN, Connection Type, Domain, and ISP
data/Gemfile CHANGED
@@ -2,8 +2,4 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- gem 'maxmind-db'
6
- gem 'minitest', group: :development
7
- gem 'rake', group: :development
8
- gem 'rubocop', group: :development
9
- gem 'rubocop-performance', group: :development
5
+ gemspec
data/README.dev.md CHANGED
@@ -1,4 +1,4 @@
1
1
  # How to release
2
2
 
3
3
  See
4
- [here](https://github.com/maxmind/MaxMind-DB-Reader-ruby/blob/master/README.dev.md).
4
+ [here](https://github.com/maxmind/MaxMind-DB-Reader-ruby/blob/main/README.dev.md).
data/README.md CHANGED
@@ -47,7 +47,9 @@ for more details.
47
47
  require 'maxmind/geoip2'
48
48
 
49
49
  # This creates the Reader object which should be reused across lookups.
50
- reader = MaxMind::GeoIP2::Reader.new('/usr/share/GeoIP/GeoIP2-City.mmdb')
50
+ reader = MaxMind::GeoIP2::Reader.new(
51
+ database: '/usr/share/GeoIP/GeoIP2-City.mmdb',
52
+ )
51
53
 
52
54
  record = reader.city('128.101.101.101')
53
55
 
@@ -74,7 +76,9 @@ puts record.traits.network # 128.101.101.101/32
74
76
  require 'maxmind/geoip2'
75
77
 
76
78
  # This creates the Reader object which should be reused across lookups.
77
- reader = MaxMind::GeoIP2::Reader.new('/usr/share/GeoIP/GeoIP2-Country.mmdb')
79
+ reader = MaxMind::GeoIP2::Reader.new(
80
+ database: '/usr/share/GeoIP/GeoIP2-Country.mmdb',
81
+ )
78
82
 
79
83
  record = reader.country('128.101.101.101')
80
84
 
@@ -89,7 +93,9 @@ puts record.country.names['zh-CN'] # '美国'
89
93
  require 'maxmind/geoip2'
90
94
 
91
95
  # This creates the Reader object which should be reused across lookups.
92
- reader = MaxMind::GeoIP2::Reader.new('/usr/share/GeoIP/GeoIP2-Enterprise.mmdb')
96
+ reader = MaxMind::GeoIP2::Reader.new(
97
+ database: '/usr/share/GeoIP/GeoIP2-Enterprise.mmdb',
98
+ )
93
99
 
94
100
  record = reader.enterprise('128.101.101.101')
95
101
 
@@ -120,7 +126,9 @@ puts record.traits.network # 128.101.101.101/32
120
126
  require 'maxmind/geoip2'
121
127
 
122
128
  # This creates the Reader object which should be reused across lookups.
123
- reader = MaxMind::GeoIP2::Reader.new('/usr/share/GeoIP/GeoIP2-Anonymous-IP.mmdb')
129
+ reader = MaxMind::GeoIP2::Reader.new(
130
+ database: '/usr/share/GeoIP/GeoIP2-Anonymous-IP.mmdb',
131
+ )
124
132
 
125
133
  record = reader.anonymous_ip('128.101.101.101')
126
134
 
@@ -133,7 +141,9 @@ puts "Anonymous" if record.is_anonymous
133
141
  require 'maxmind/geoip2'
134
142
 
135
143
  # This creates the Reader object which should be reused across lookups.
136
- reader = MaxMind::GeoIP2::Reader.new('/usr/share/GeoIP/GeoLite2-ASN.mmdb')
144
+ reader = MaxMind::GeoIP2::Reader.new(
145
+ database: '/usr/share/GeoIP/GeoLite2-ASN.mmdb',
146
+ )
137
147
 
138
148
  record = reader.asn('128.101.101.101')
139
149
 
@@ -147,7 +157,9 @@ puts record.autonomous_system_organization # Example Ltd
147
157
  require 'maxmind/geoip2'
148
158
 
149
159
  # This creates the Reader object which should be reused across lookups.
150
- reader = MaxMind::GeoIP2::Reader.new('/usr/share/GeoIP/GeoIP2-Connection-Type.mmdb')
160
+ reader = MaxMind::GeoIP2::Reader.new(
161
+ database: '/usr/share/GeoIP/GeoIP2-Connection-Type.mmdb',
162
+ )
151
163
 
152
164
  record = reader.connection_type('128.101.101.101')
153
165
 
@@ -160,7 +172,9 @@ puts record.connection_type # Cable/DSL
160
172
  require 'maxmind/geoip2'
161
173
 
162
174
  # This creates the Reader object which should be reused across lookups.
163
- reader = MaxMind::GeoIP2::Reader.new('/usr/share/GeoIP/GeoIP2-Domain.mmdb')
175
+ reader = MaxMind::GeoIP2::Reader.new(
176
+ database: '/usr/share/GeoIP/GeoIP2-Domain.mmdb',
177
+ )
164
178
 
165
179
  record = reader.domain('128.101.101.101')
166
180
 
@@ -173,7 +187,9 @@ puts record.domain # example.com
173
187
  require 'maxmind/geoip2'
174
188
 
175
189
  # This creates the Reader object which should be reused across lookups.
176
- reader = MaxMind::GeoIP2::Reader.new('/usr/share/GeoIP/GeoIP2-ISP.mmdb')
190
+ reader = MaxMind::GeoIP2::Reader.new(
191
+ database: '/usr/share/GeoIP/GeoIP2-ISP.mmdb',
192
+ )
177
193
 
178
194
  record = reader.isp('128.101.101.101')
179
195
 
@@ -183,6 +199,64 @@ puts record.isp # University of Minnesota
183
199
  puts record.organization # University of Minnesota
184
200
  ```
185
201
 
202
+ ## Web Service Client
203
+
204
+ ### Usage
205
+
206
+ To use this API, you must create a new `MaxMind::GeoIP2::Client` object
207
+ with your account ID and license key. To use the GeoLite2 web service, you
208
+ may also set the `host` parameter to `geolite.info`. You may then you call
209
+ the method corresponding to a specific end point, passing it the IP address
210
+ you want to look up.
211
+
212
+ If the request succeeds, the method call will return a model class for the end
213
+ point you called. This model in turn contains multiple record classes, each of
214
+ which represents part of the data returned by the web service.
215
+
216
+ If there is an error, a structured exception is thrown.
217
+
218
+ See the [API documentation](https://www.rubydoc.info/gems/maxmind-geoip2)
219
+ for more details.
220
+
221
+ ### Example
222
+
223
+ ```ruby
224
+ require 'maxmind/geoip2'
225
+
226
+ # This creates a Client object that can be reused across requests.
227
+ # Replace "42" with your account ID and "license_key" with your license
228
+ # key.
229
+ client = MaxMind::GeoIP2::Client.new(
230
+ account_id: 42,
231
+ license_key: 'license_key',
232
+
233
+ # To use the GeoLite2 web service instead of GeoIP2 Precision, set
234
+ # the host parameter to "geolite.info":
235
+ # host: 'geolite.info',
236
+ )
237
+
238
+ # Replace "city" with the method corresponding to the web service that
239
+ # you are using, e.g., "country", "insights". Please note that Insights
240
+ # is only supported by GeoIP2 Precision and not the GeoLite2 web service.
241
+ record = client.city('128.101.101.101')
242
+
243
+ puts record.country.iso_code # US
244
+ puts record.country.name # United States
245
+ puts record.country.names['zh-CN'] # 美国
246
+
247
+ puts record.most_specific_subdivision.name # Minnesota
248
+ puts record.most_specific_subdivision.iso_code # MN
249
+
250
+ puts record.city.name # Minneapolis
251
+
252
+ puts record.postal.code # 55455
253
+
254
+ puts record.location.latitude # 44.9733
255
+ puts record.location.longitude # -93.2323
256
+
257
+ puts record.traits.network # 128.101.101.101/32
258
+ ```
259
+
186
260
  ## Values to use for Database or Array Keys
187
261
 
188
262
  **We strongly discourage you from using a value from any `names` property
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'maxmind/geoip2/client'
3
4
  require 'maxmind/geoip2/reader'
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'connection_pool'
4
+ require 'http'
5
+ require 'json'
6
+ require 'maxmind/geoip2/errors'
7
+ require 'maxmind/geoip2/model/city'
8
+ require 'maxmind/geoip2/model/country'
9
+ require 'maxmind/geoip2/model/insights'
10
+
11
+ module MaxMind
12
+ module GeoIP2
13
+ # This class provides a client API for all the
14
+ # {https://dev.maxmind.com/geoip/geoip2/web-services/ GeoIP2 Precision web
15
+ # services}. The services are Country, City, and Insights. Each service
16
+ # returns a different set of data about an IP address, with Country returning
17
+ # the least data and Insights the most.
18
+ #
19
+ # Each web service is represented by a different model class, and these model
20
+ # classes in turn contain multiple record classes. The record classes have
21
+ # attributes which contain data about the IP address.
22
+ #
23
+ # If the web service does not return a particular piece of data for an IP
24
+ # address, the associated attribute is not populated.
25
+ #
26
+ # The web service may not return any information for an entire record, in
27
+ # which case all of the attributes for that record class will be empty.
28
+ #
29
+ # == Usage
30
+ #
31
+ # The basic API for this class is the same for all of the web service end
32
+ # points. First you create a web service client object with your MaxMind
33
+ # account ID and license key, then you call the method corresponding to a
34
+ # specific end point, passing it the IP address you want to look up.
35
+ #
36
+ # If the request succeeds, the method call will return a model class for the
37
+ # service you called. This model in turn contains multiple record classes,
38
+ # each of which represents part of the data returned by the web service.
39
+ #
40
+ # If the request fails, the client class throws an exception.
41
+ #
42
+ # == Example
43
+ #
44
+ # require 'maxmind/geoip2'
45
+ #
46
+ # client = MaxMind::GeoIP2::Client.new(
47
+ # account_id: 42,
48
+ # license_key: 'abcdef123456',
49
+ # )
50
+ #
51
+ # # Replace 'city' with the method corresponding to the web service you
52
+ # # are using, e.g., 'country', 'insights'.
53
+ # record = client.city('128.101.101.101')
54
+ #
55
+ # puts record.country.iso_code
56
+ class Client
57
+ # rubocop:disable Metrics/ParameterLists
58
+
59
+ # Create a Client that may be used to query a GeoIP2 Precision web service.
60
+ #
61
+ # Once created, the Client is safe to use for lookups from multiple
62
+ # threads.
63
+ #
64
+ # @param account_id [Integer] your MaxMind account ID.
65
+ #
66
+ # @param license_key [String] your MaxMind license key.
67
+ #
68
+ # @param locales [Array<String>] a list of locale codes to use in the name
69
+ # property from most preferred to least preferred.
70
+ #
71
+ # @param host [String] the host to use when querying the web service. Set
72
+ # this to "geolite.info" to use the GeoLite2 web service instead of
73
+ # GeoIP2 Precision.
74
+ #
75
+ # @param timeout [Integer] the number of seconds to wait for a request
76
+ # before timing out. If 0, no timeout is set.
77
+ #
78
+ # @param proxy_address [String] proxy address to use, if any.
79
+ #
80
+ # @param proxy_port [Integer] proxy port to use, if any.
81
+ #
82
+ # @param proxy_username [String] proxy username to use, if any.
83
+ #
84
+ # @param proxy_password [String] proxy password to use, if any.
85
+ #
86
+ # @param pool_size [Integer] HTTP connection pool size
87
+ def initialize(
88
+ account_id:,
89
+ license_key:,
90
+ locales: ['en'],
91
+ host: 'geoip.maxmind.com',
92
+ timeout: 0,
93
+ proxy_address: '',
94
+ proxy_port: 0,
95
+ proxy_username: '',
96
+ proxy_password: '',
97
+ pool_size: 5
98
+ )
99
+ @account_id = account_id
100
+ @license_key = license_key
101
+ @locales = locales
102
+ @host = host
103
+ @timeout = timeout
104
+ @proxy_address = proxy_address
105
+ @proxy_port = proxy_port
106
+ @proxy_username = proxy_username
107
+ @proxy_password = proxy_password
108
+ @pool_size = pool_size
109
+
110
+ @connection_pool = ConnectionPool.new(size: @pool_size) do
111
+ make_http_client.persistent("https://#{@host}")
112
+ end
113
+ end
114
+ # rubocop:enable Metrics/ParameterLists
115
+
116
+ # This method calls the City web service.
117
+ #
118
+ # @param ip_address [String] IPv4 or IPv6 address as a string. If no
119
+ # address is provided, the address that the web service is called from is
120
+ # used.
121
+ #
122
+ # @raise [HTTP::Error] if there was an error performing the HTTP request,
123
+ # such as an error connecting.
124
+ #
125
+ # @raise [JSON::ParserError] if there was invalid JSON in the response.
126
+ #
127
+ # @raise [HTTPError] if there was a problem with the HTTP response, such as
128
+ # an unexpected HTTP status code.
129
+ #
130
+ # @raise [AddressInvalidError] if the web service believes the IP address
131
+ # to be invalid or missing.
132
+ #
133
+ # @raise [AddressNotFoundError] if the IP address was not found.
134
+ #
135
+ # @raise [AddressReservedError] if the IP address is reserved.
136
+ #
137
+ # @raise [AuthenticationError] if there was a problem authenticating to the
138
+ # web service, such as an invalid or missing license key.
139
+ #
140
+ # @raise [InsufficientFundsError] if your account is out of credit.
141
+ #
142
+ # @raise [PermissionRequiredError] if your account does not have permission
143
+ # to use the web service.
144
+ #
145
+ # @raise [InvalidRequestError] if the web service responded with an error
146
+ # and there is no more specific error to raise.
147
+ #
148
+ # @return [MaxMind::GeoIP2::Model::City]
149
+ def city(ip_address = 'me')
150
+ response_for('city', MaxMind::GeoIP2::Model::City, ip_address)
151
+ end
152
+
153
+ # This method calls the Country web service.
154
+ #
155
+ # @param ip_address [String] IPv4 or IPv6 address as a string. If no
156
+ # address is provided, the address that the web service is called from is
157
+ # used.
158
+ #
159
+ # @raise [HTTP::Error] if there was an error performing the HTTP request,
160
+ # such as an error connecting.
161
+ #
162
+ # @raise [JSON::ParserError] if there was invalid JSON in the response.
163
+ #
164
+ # @raise [HTTPError] if there was a problem with the HTTP response, such as
165
+ # an unexpected HTTP status code.
166
+ #
167
+ # @raise [AddressInvalidError] if the web service believes the IP address
168
+ # to be invalid or missing.
169
+ #
170
+ # @raise [AddressNotFoundError] if the IP address was not found.
171
+ #
172
+ # @raise [AddressReservedError] if the IP address is reserved.
173
+ #
174
+ # @raise [AuthenticationError] if there was a problem authenticating to the
175
+ # web service, such as an invalid or missing license key.
176
+ #
177
+ # @raise [InsufficientFundsError] if your account is out of credit.
178
+ #
179
+ # @raise [PermissionRequiredError] if your account does not have permission
180
+ # to use the web service.
181
+ #
182
+ # @raise [InvalidRequestError] if the web service responded with an error
183
+ # and there is no more specific error to raise.
184
+ #
185
+ # @return [MaxMind::GeoIP2::Model::Country]
186
+ def country(ip_address = 'me')
187
+ response_for('country', MaxMind::GeoIP2::Model::Country, ip_address)
188
+ end
189
+
190
+ # This method calls the Insights web service.
191
+ #
192
+ # Insights is only supported by the GeoIP2 Precision web service. The
193
+ # GeoLite2 web service does not support it.
194
+ #
195
+ # @param ip_address [String] IPv4 or IPv6 address as a string. If no
196
+ # address is provided, the address that the web service is called from is
197
+ # used.
198
+ #
199
+ # @raise [HTTP::Error] if there was an error performing the HTTP request,
200
+ # such as an error connecting.
201
+ #
202
+ # @raise [JSON::ParserError] if there was invalid JSON in the response.
203
+ #
204
+ # @raise [HTTPError] if there was a problem with the HTTP response, such as
205
+ # an unexpected HTTP status code.
206
+ #
207
+ # @raise [AddressInvalidError] if the web service believes the IP address
208
+ # to be invalid or missing.
209
+ #
210
+ # @raise [AddressNotFoundError] if the IP address was not found.
211
+ #
212
+ # @raise [AddressReservedError] if the IP address is reserved.
213
+ #
214
+ # @raise [AuthenticationError] if there was a problem authenticating to the
215
+ # web service, such as an invalid or missing license key.
216
+ #
217
+ # @raise [InsufficientFundsError] if your account is out of credit.
218
+ #
219
+ # @raise [PermissionRequiredError] if your account does not have permission
220
+ # to use the web service.
221
+ #
222
+ # @raise [InvalidRequestError] if the web service responded with an error
223
+ # and there is no more specific error to raise.
224
+ #
225
+ # @return [MaxMind::GeoIP2::Model::Insights]
226
+ def insights(ip_address = 'me')
227
+ response_for('insights', MaxMind::GeoIP2::Model::Insights, ip_address)
228
+ end
229
+
230
+ private
231
+
232
+ def response_for(endpoint, model_class, ip_address)
233
+ record = get(endpoint, ip_address)
234
+
235
+ model_class.new(record, @locales)
236
+ end
237
+
238
+ def make_http_client
239
+ headers = HTTP.basic_auth(user: @account_id, pass: @license_key)
240
+ .headers(
241
+ accept: 'application/json',
242
+ user_agent: 'MaxMind-GeoIP2-ruby',
243
+ )
244
+ timeout = @timeout > 0 ? headers.timeout(@timeout) : headers
245
+
246
+ proxy = timeout
247
+ if @proxy_address != ''
248
+ proxy_params = [@proxy_address]
249
+ proxy_params << (@proxy_port == 0 ? nil : @proxy_port)
250
+ proxy_params << (@proxy_username == '' ? nil : @proxy_username)
251
+ proxy_params << (@proxy_password == '' ? nil : @proxy_password)
252
+ proxy = timeout.via(*proxy_params)
253
+ end
254
+
255
+ proxy
256
+ end
257
+
258
+ def get(endpoint, ip_address)
259
+ url = "/geoip/v2.1/#{endpoint}/#{ip_address}"
260
+
261
+ response = nil
262
+ body = nil
263
+ @connection_pool.with do |client|
264
+ response = client.get(url)
265
+ body = response.to_s
266
+ end
267
+
268
+ is_json = response.headers[:content_type]&.include?('json')
269
+
270
+ if response.status.client_error?
271
+ return handle_client_error(endpoint, response.code, body, is_json)
272
+ end
273
+
274
+ if response.status.server_error?
275
+ raise HTTPError,
276
+ "Received server error response (#{response.code}) for #{endpoint} with body #{body}"
277
+ end
278
+
279
+ if response.code != 200
280
+ raise HTTPError,
281
+ "Received unexpected response (#{response.code}) for #{endpoint} with body #{body}"
282
+ end
283
+
284
+ handle_success(endpoint, body, is_json)
285
+ end
286
+
287
+ # rubocop:disable Metrics/CyclomaticComplexity
288
+ def handle_client_error(endpoint, status, body, is_json)
289
+ if !is_json
290
+ raise HTTPError,
291
+ "Received client error response (#{status}) for #{endpoint} but it is not JSON: #{body}"
292
+ end
293
+
294
+ error = JSON.parse(body)
295
+
296
+ if !error.key?('code') || !error.key?('error')
297
+ raise HTTPError,
298
+ "Received client error response (#{status}) that is JSON but does not specify code or error keys: #{body}"
299
+ end
300
+
301
+ case error['code']
302
+ when 'IP_ADDRESS_INVALID', 'IP_ADDRESS_REQUIRED'
303
+ raise AddressInvalidError, error['error']
304
+ when 'IP_ADDRESS_NOT_FOUND'
305
+ raise AddressNotFoundError, error['error']
306
+ when 'IP_ADDRESS_RESERVED'
307
+ raise AddressReservedError, error['error']
308
+ when 'ACCOUNT_ID_REQUIRED',
309
+ 'ACCOUNT_ID_UNKNOWN',
310
+ 'AUTHORIZATION_INVALID',
311
+ 'LICENSE_KEY_REQUIRED'
312
+ raise AuthenticationError, error['error']
313
+ when 'INSUFFICIENT_FUNDS'
314
+ raise InsufficientFundsError, error['error']
315
+ when 'PERMISSION_REQUIRED'
316
+ raise PermissionRequiredError, error['error']
317
+ else
318
+ raise InvalidRequestError, error['error']
319
+ end
320
+ end
321
+ # rubocop:enable Metrics/CyclomaticComplexity
322
+
323
+ def handle_success(endpoint, body, is_json)
324
+ if !is_json
325
+ raise HTTPError,
326
+ "Received a success response for #{endpoint} but it is not JSON: #{body}"
327
+ end
328
+
329
+ JSON.parse(body)
330
+ end
331
+ end
332
+ end
333
+ end