maxmind-geoip2 0.2.0 → 0.7.0

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