maxmind-geoip2 0.1.0 → 0.6.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.
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