maxmind-geoip2 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/CLAUDE.md +390 -0
- data/Gemfile.lock +40 -28
- data/README.md +1 -1
- data/Rakefile +1 -2
- data/lib/maxmind/geoip2/model/insights.rb +14 -0
- data/lib/maxmind/geoip2/record/anonymizer.rb +105 -0
- data/lib/maxmind/geoip2/record/traits.rb +37 -0
- data/lib/maxmind/geoip2/version.rb +1 -1
- data/maxmind-geoip2.gemspec +5 -2
- data/test/test_client.rb +31 -6
- data/test/test_model_country.rb +13 -14
- data/test/test_model_names.rb +3 -0
- data/test/test_reader.rb +43 -29
- metadata +49 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f26229b50b6ba4760df9389e76c5e705dc2b5b53e7fc50eb6be25d2af74f6985
|
|
4
|
+
data.tar.gz: bfcb0fef72edaf05cdfcb1cc55be5ce2d12afbc650912e062ae263649a279736
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d3de90ee7df88d6acbef1af76202af2130e9670ad726bea5f9753581bd5e3ae962f859720458f833731f0822cc08345171ac90cde7b446c784ca7c6ca21215c
|
|
7
|
+
data.tar.gz: 34b7484739814ecd6de33435310c8cb29840534c4beed8d063c2ed00f2e0eee46338cec95bef1be5acb2ac2947be937842a2edc1f646b46460d70fa656213f6d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.0 (2025-11-20)
|
|
4
|
+
|
|
5
|
+
* Ruby 3.2+ is now required. If you're using Ruby 3.0 or 3.1, please use
|
|
6
|
+
version 1.3.0 of this gem.
|
|
7
|
+
* A new `anonymizer` object has been added to the `MaxMind::GeoIP2::Model::Insights`
|
|
8
|
+
model. This object indicates whether the IP address is part of an anonymizing
|
|
9
|
+
network, including VPN confidence scoring, provider name detection, and network
|
|
10
|
+
last seen date. This is only available from the GeoIP2 Insights web service.
|
|
11
|
+
* A new `ip_risk_snapshot` method has been added to `MaxMind::GeoIP2::Record::Traits`.
|
|
12
|
+
This field contains the risk associated with the IP address, ranging from 0.01 to
|
|
13
|
+
99 (a higher score indicates a higher risk). This is only available from the GeoIP2
|
|
14
|
+
Insights web service.
|
|
15
|
+
* The `anonymous?`, `anonymous_vpn?`, `hosting_provider?`, `public_proxy?`,
|
|
16
|
+
`residential_proxy?`, and `tor_exit_node?` methods in
|
|
17
|
+
`MaxMind::GeoIP2::Record::Traits` have been deprecated. Please use the
|
|
18
|
+
corresponding methods in the `anonymizer` object from the GeoIP2 Insights
|
|
19
|
+
response instead.
|
|
20
|
+
|
|
21
|
+
|
|
3
22
|
## 1.3.0 (2025-05-06)
|
|
4
23
|
|
|
5
24
|
* Support for the GeoIP Anonymous Plus database has been added. To do a
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
**GeoIP2-ruby** is MaxMind's official Ruby client library for:
|
|
8
|
+
- **GeoIP2/GeoLite2 Web Services**: Country, City, and Insights endpoints
|
|
9
|
+
- **GeoIP2/GeoLite2 Databases**: Local MMDB file reading for various database types (City, Country, ASN, Anonymous IP, Anonymous Plus, ISP, etc.)
|
|
10
|
+
|
|
11
|
+
The library provides both web service clients and database readers that return strongly-typed model objects containing geographic, ISP, anonymizer, and other IP-related data.
|
|
12
|
+
|
|
13
|
+
**Key Technologies:**
|
|
14
|
+
- Ruby 3.2+ (uses frozen string literals and modern Ruby features)
|
|
15
|
+
- MaxMind DB Reader for binary database files
|
|
16
|
+
- HTTP gem for web service client functionality
|
|
17
|
+
- Minitest for testing
|
|
18
|
+
- RuboCop with multiple plugins for code quality
|
|
19
|
+
|
|
20
|
+
## Code Architecture
|
|
21
|
+
|
|
22
|
+
### Package Structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
lib/maxmind/geoip2/
|
|
26
|
+
├── model/ # Response models (City, Insights, AnonymousIP, etc.)
|
|
27
|
+
├── record/ # Data records (City, Location, Traits, etc.)
|
|
28
|
+
├── client.rb # HTTP client for MaxMind web services
|
|
29
|
+
├── reader.rb # Local MMDB file reader
|
|
30
|
+
├── errors.rb # Custom exceptions for error handling
|
|
31
|
+
└── version.rb # Version constant
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Key Design Patterns
|
|
35
|
+
|
|
36
|
+
#### 1. **Attr Reader Pattern for Immutable Data**
|
|
37
|
+
|
|
38
|
+
Models expose data through `attr_reader` attributes that are initialized in the constructor. Unlike PHP's readonly properties, Ruby uses instance variables with attr_reader:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
class City < Country
|
|
42
|
+
attr_reader :city
|
|
43
|
+
attr_reader :location
|
|
44
|
+
attr_reader :postal
|
|
45
|
+
attr_reader :subdivisions
|
|
46
|
+
|
|
47
|
+
def initialize(record, locales)
|
|
48
|
+
super
|
|
49
|
+
@city = MaxMind::GeoIP2::Record::City.new(record['city'], locales)
|
|
50
|
+
@location = MaxMind::GeoIP2::Record::Location.new(record['location'])
|
|
51
|
+
@postal = MaxMind::GeoIP2::Record::Postal.new(record['postal'])
|
|
52
|
+
@subdivisions = create_subdivisions(record['subdivisions'], locales)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Key Points:**
|
|
58
|
+
- Instance variables are set in the constructor
|
|
59
|
+
- Use `attr_reader` to expose them
|
|
60
|
+
- Models and records are initialized from hash data (from JSON/DB)
|
|
61
|
+
- Records are composed objects (City contains City record, Location record, etc.)
|
|
62
|
+
|
|
63
|
+
#### 2. **Inheritance Hierarchies**
|
|
64
|
+
|
|
65
|
+
Models follow clear inheritance patterns:
|
|
66
|
+
- `Country` → base model with country/continent data
|
|
67
|
+
- `City` extends `Country` → adds city, location, postal, subdivisions
|
|
68
|
+
- `Insights` extends `City` → adds additional web service fields (web service only)
|
|
69
|
+
- `Enterprise` extends `City` → adds enterprise-specific fields
|
|
70
|
+
|
|
71
|
+
Records have similar patterns:
|
|
72
|
+
- `Abstract` → base with `get` method for accessing hash data
|
|
73
|
+
- `Place` extends `Abstract` → adds names/locales handling
|
|
74
|
+
- Specific records (`City`, `Country`, etc.) extend `Place` or `Abstract`
|
|
75
|
+
|
|
76
|
+
#### 3. **Get Method Pattern for Data Access**
|
|
77
|
+
|
|
78
|
+
Both models and records use a protected `get` method to safely access hash data:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
def get(key)
|
|
82
|
+
if @record.nil? || !@record.key?(key)
|
|
83
|
+
return false if key.start_with?('is_')
|
|
84
|
+
return nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
@record[key]
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- Returns `false` for missing boolean fields (starting with `is_`)
|
|
92
|
+
- Returns `nil` for missing optional fields
|
|
93
|
+
- Records store the raw hash in `@record` instance variable
|
|
94
|
+
|
|
95
|
+
Public methods expose data through the `get` method:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
def anonymizer_confidence
|
|
99
|
+
get('anonymizer_confidence')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def provider_name
|
|
103
|
+
get('provider_name')
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### 4. **Lazy Parsing for Special Types**
|
|
108
|
+
|
|
109
|
+
Some fields require parsing and are computed lazily:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
def network_last_seen
|
|
113
|
+
return @network_last_seen if defined?(@network_last_seen)
|
|
114
|
+
|
|
115
|
+
date_string = get('network_last_seen')
|
|
116
|
+
|
|
117
|
+
if !date_string
|
|
118
|
+
return nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@network_last_seen = Date.parse(date_string)
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- Use `defined?(@variable)` to check if already parsed
|
|
126
|
+
- Parse only once and cache in instance variable
|
|
127
|
+
- Handle nil cases before parsing
|
|
128
|
+
|
|
129
|
+
#### 5. **Web Service Only vs Database Models**
|
|
130
|
+
|
|
131
|
+
Some models are only used by web services and do **not** need MaxMind DB support:
|
|
132
|
+
|
|
133
|
+
**Web Service Only Models**:
|
|
134
|
+
- Models that are exclusive to web service responses
|
|
135
|
+
- Simpler implementation, just inherit and define in model hierarchy
|
|
136
|
+
- Example: `Insights` (extends City but used only for web service)
|
|
137
|
+
|
|
138
|
+
**Database-Supported Models**:
|
|
139
|
+
- Models used by both web services and database files
|
|
140
|
+
- Reader has specific methods (e.g., `anonymous_ip`, `anonymous_plus`, `city`)
|
|
141
|
+
- Must handle MaxMind DB format data structures
|
|
142
|
+
- Example: `City`, `Country`, `AnonymousIP`, `AnonymousPlus`
|
|
143
|
+
|
|
144
|
+
## Testing Conventions
|
|
145
|
+
|
|
146
|
+
### Running Tests
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Install dependencies
|
|
150
|
+
bundle install
|
|
151
|
+
|
|
152
|
+
# Run all tests
|
|
153
|
+
bundle exec rake test
|
|
154
|
+
|
|
155
|
+
# Run tests and RuboCop
|
|
156
|
+
bundle exec rake # default task
|
|
157
|
+
|
|
158
|
+
# Run RuboCop only
|
|
159
|
+
bundle exec rake rubocop
|
|
160
|
+
|
|
161
|
+
# Run specific test file
|
|
162
|
+
ruby -Ilib:test test/test_reader.rb
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Test Structure
|
|
166
|
+
|
|
167
|
+
Tests are organized by functionality:
|
|
168
|
+
- `test/test_reader.rb` - Database reader tests
|
|
169
|
+
- `test/test_client.rb` - Web service client tests
|
|
170
|
+
- `test/test_model_*.rb` - Model-specific tests
|
|
171
|
+
- `test/data/` - Test fixtures and sample database files
|
|
172
|
+
|
|
173
|
+
### Test Patterns
|
|
174
|
+
|
|
175
|
+
Tests use Minitest with a constant for test data:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
class CountryModelTest < Minitest::Test
|
|
179
|
+
RAW = {
|
|
180
|
+
'continent' => {
|
|
181
|
+
'code' => 'NA',
|
|
182
|
+
'geoname_id' => 42,
|
|
183
|
+
'names' => { 'en' => 'North America' },
|
|
184
|
+
},
|
|
185
|
+
'country' => {
|
|
186
|
+
'geoname_id' => 1,
|
|
187
|
+
'iso_code' => 'US',
|
|
188
|
+
'names' => { 'en' => 'United States of America' },
|
|
189
|
+
},
|
|
190
|
+
'traits' => {
|
|
191
|
+
'ip_address' => '1.2.3.4',
|
|
192
|
+
'prefix_length' => 24,
|
|
193
|
+
},
|
|
194
|
+
}.freeze
|
|
195
|
+
|
|
196
|
+
def test_values
|
|
197
|
+
model = MaxMind::GeoIP2::Model::Country.new(RAW, ['en'])
|
|
198
|
+
|
|
199
|
+
assert_equal(42, model.continent.geoname_id)
|
|
200
|
+
assert_equal('NA', model.continent.code)
|
|
201
|
+
assert_equal('United States of America', model.country.name)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
When adding new fields to models:
|
|
207
|
+
1. Update the `RAW` constant to include the new field
|
|
208
|
+
2. Add assertions to verify the field is properly populated
|
|
209
|
+
3. Test both presence and absence of the field (nil handling)
|
|
210
|
+
4. Test with different values if applicable
|
|
211
|
+
|
|
212
|
+
## Working with This Codebase
|
|
213
|
+
|
|
214
|
+
### Adding New Fields to Existing Models
|
|
215
|
+
|
|
216
|
+
For database models (like AnonymousPlus):
|
|
217
|
+
|
|
218
|
+
1. **Add a public method** that calls `get`:
|
|
219
|
+
```ruby
|
|
220
|
+
# A description of the field.
|
|
221
|
+
#
|
|
222
|
+
# @return [Type, nil]
|
|
223
|
+
def field_name
|
|
224
|
+
get('field_name')
|
|
225
|
+
end
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
2. **For fields requiring parsing** (dates, complex types), use lazy loading:
|
|
229
|
+
```ruby
|
|
230
|
+
def network_last_seen
|
|
231
|
+
return @network_last_seen if defined?(@network_last_seen)
|
|
232
|
+
|
|
233
|
+
date_string = get('network_last_seen')
|
|
234
|
+
|
|
235
|
+
if !date_string
|
|
236
|
+
return nil
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
@network_last_seen = Date.parse(date_string)
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
For composed models (like City, Country):
|
|
244
|
+
|
|
245
|
+
1. **Add `attr_reader`** for the new record/field:
|
|
246
|
+
```ruby
|
|
247
|
+
attr_reader :new_field
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
2. **Initialize in constructor**:
|
|
251
|
+
```ruby
|
|
252
|
+
def initialize(record, locales)
|
|
253
|
+
super
|
|
254
|
+
@new_field = record['new_field']
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
3. **Provide comprehensive YARD documentation** (`@return` tags)
|
|
259
|
+
4. **Update tests** to include the new field in test data and assertions
|
|
260
|
+
5. **Update CHANGELOG.md** with the change
|
|
261
|
+
|
|
262
|
+
### Adding New Models
|
|
263
|
+
|
|
264
|
+
When creating a new model class:
|
|
265
|
+
|
|
266
|
+
1. **Determine if web service only or database-supported**
|
|
267
|
+
2. **Follow the pattern** from existing similar models
|
|
268
|
+
3. **Extend the appropriate base class** (e.g., `Country`, `City`, or standalone)
|
|
269
|
+
4. **Use `attr_reader`** for composed record objects
|
|
270
|
+
5. **Provide comprehensive YARD documentation** for all public methods
|
|
271
|
+
6. **Add corresponding tests** with full coverage
|
|
272
|
+
7. **If database-supported**, add a method to `Reader` class
|
|
273
|
+
|
|
274
|
+
### Deprecation Guidelines
|
|
275
|
+
|
|
276
|
+
When deprecating fields:
|
|
277
|
+
|
|
278
|
+
1. **Use `@deprecated` in YARD doc** with version and alternative:
|
|
279
|
+
```ruby
|
|
280
|
+
# This field is deprecated as of version 2.0.0.
|
|
281
|
+
# Use the anonymizer object from the Insights response instead.
|
|
282
|
+
#
|
|
283
|
+
# @return [Boolean]
|
|
284
|
+
# @deprecated since 2.0.0
|
|
285
|
+
def is_anonymous
|
|
286
|
+
get('is_anonymous')
|
|
287
|
+
end
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
2. **Keep deprecated fields functional** - don't break existing code
|
|
291
|
+
3. **Update CHANGELOG.md** with deprecation notices
|
|
292
|
+
4. **Document alternatives** in the deprecation message
|
|
293
|
+
|
|
294
|
+
### CHANGELOG.md Format
|
|
295
|
+
|
|
296
|
+
Always update `CHANGELOG.md` for user-facing changes.
|
|
297
|
+
|
|
298
|
+
**Important**: Do not add a date to changelog entries until release time.
|
|
299
|
+
|
|
300
|
+
- If there's an existing version entry without a date (e.g., `1.5.0`), add your changes there
|
|
301
|
+
- If creating a new version entry, don't include a date - it will be added at release time
|
|
302
|
+
- Use past tense for descriptions
|
|
303
|
+
|
|
304
|
+
```markdown
|
|
305
|
+
## 1.5.0
|
|
306
|
+
|
|
307
|
+
* A new `field_name` method has been added to `MaxMind::GeoIP2::Model::ModelName`.
|
|
308
|
+
This method provides information about...
|
|
309
|
+
* The `old_field` method in `MaxMind::GeoIP2::Model::ModelName` has been deprecated.
|
|
310
|
+
Please use `new_field` instead.
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Common Pitfalls and Solutions
|
|
314
|
+
|
|
315
|
+
### Problem: Incorrect Nil Handling
|
|
316
|
+
|
|
317
|
+
Using the wrong nil check can cause unexpected behavior.
|
|
318
|
+
|
|
319
|
+
**Solution**: Follow these patterns:
|
|
320
|
+
- Use `if !variable` or `if variable.nil?` to check for nil
|
|
321
|
+
- The `get` method returns `nil` for missing keys (except `is_*` keys which return `false`)
|
|
322
|
+
- Use `defined?(@variable)` to check if an instance variable has been set (for lazy loading)
|
|
323
|
+
|
|
324
|
+
### Problem: Missing YARD Documentation
|
|
325
|
+
|
|
326
|
+
New methods without documentation make the API harder to use.
|
|
327
|
+
|
|
328
|
+
**Solution**: Always add YARD documentation:
|
|
329
|
+
- Use `@return [Type, nil]` for the return type
|
|
330
|
+
- Add a description of what the method returns
|
|
331
|
+
- Use `@deprecated since X.Y.Z` for deprecated methods
|
|
332
|
+
- Include examples in model class documentation if helpful
|
|
333
|
+
|
|
334
|
+
### Problem: Test Failures After Adding Fields
|
|
335
|
+
|
|
336
|
+
Tests fail because fixtures don't include new fields.
|
|
337
|
+
|
|
338
|
+
**Solution**: Update all related tests:
|
|
339
|
+
1. Add field to test `RAW` constant or test data hash
|
|
340
|
+
2. Add assertions for the new field
|
|
341
|
+
3. Test nil case if field is optional
|
|
342
|
+
4. Test different data types if applicable
|
|
343
|
+
|
|
344
|
+
## Code Style Requirements
|
|
345
|
+
|
|
346
|
+
- **RuboCop enforced** with multiple plugins (minitest, performance, rake, thread_safety)
|
|
347
|
+
- **Frozen string literals** (`# frozen_string_literal: true`) in all files
|
|
348
|
+
- **Target Ruby 3.2+**
|
|
349
|
+
- **No metrics cops** - AbcSize, ClassLength, MethodLength disabled
|
|
350
|
+
- **Trailing commas allowed** in arrays, hashes, and arguments
|
|
351
|
+
- **Use `if !condition`** instead of `unless condition` (NegatedIf disabled)
|
|
352
|
+
|
|
353
|
+
Key RuboCop configurations:
|
|
354
|
+
- Line length not enforced
|
|
355
|
+
- Format string token checks disabled
|
|
356
|
+
- Numeric predicates allowed in any style
|
|
357
|
+
- Multiple assertions allowed in tests
|
|
358
|
+
|
|
359
|
+
## Development Workflow
|
|
360
|
+
|
|
361
|
+
### Setup
|
|
362
|
+
```bash
|
|
363
|
+
bundle install
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Before Committing
|
|
367
|
+
```bash
|
|
368
|
+
# Run tests and linting
|
|
369
|
+
bundle exec rake
|
|
370
|
+
|
|
371
|
+
# Or run separately
|
|
372
|
+
bundle exec rake test
|
|
373
|
+
bundle exec rake rubocop
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Running Single Test
|
|
377
|
+
```bash
|
|
378
|
+
ruby -Ilib:test test/test_reader.rb
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Version Requirements
|
|
382
|
+
- **Ruby 3.2+** required
|
|
383
|
+
- Target compatibility should match current supported Ruby versions (3.2-3.4)
|
|
384
|
+
|
|
385
|
+
## Additional Resources
|
|
386
|
+
|
|
387
|
+
- [API Documentation](https://www.rubydoc.info/gems/maxmind-geoip2)
|
|
388
|
+
- [GeoIP2 Web Services Docs](https://dev.maxmind.com/geoip/docs/web-services)
|
|
389
|
+
- [MaxMind DB Format](https://maxmind.github.io/MaxMind-DB/)
|
|
390
|
+
- GitHub Issues: https://github.com/maxmind/GeoIP2-ruby/issues
|
data/Gemfile.lock
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
maxmind-geoip2 (1.
|
|
4
|
+
maxmind-geoip2 (1.4.0)
|
|
5
5
|
connection_pool (~> 2.2)
|
|
6
6
|
http (>= 4.3, < 6.0)
|
|
7
|
-
maxmind-db (~> 1.
|
|
7
|
+
maxmind-db (~> 1.4)
|
|
8
8
|
|
|
9
9
|
GEM
|
|
10
10
|
remote: https://rubygems.org/
|
|
@@ -12,10 +12,9 @@ GEM
|
|
|
12
12
|
addressable (2.8.7)
|
|
13
13
|
public_suffix (>= 2.0.2, < 7.0)
|
|
14
14
|
ast (2.4.3)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
crack (1.0.0)
|
|
15
|
+
bigdecimal (3.3.1)
|
|
16
|
+
connection_pool (2.5.4)
|
|
17
|
+
crack (1.0.1)
|
|
19
18
|
bigdecimal
|
|
20
19
|
rexml
|
|
21
20
|
domain_name (0.6.20240107)
|
|
@@ -33,36 +32,35 @@ GEM
|
|
|
33
32
|
ffi-compiler (1.3.2)
|
|
34
33
|
ffi (>= 1.15.5)
|
|
35
34
|
rake
|
|
36
|
-
hashdiff (1.1
|
|
37
|
-
http (5.
|
|
35
|
+
hashdiff (1.2.1)
|
|
36
|
+
http (5.3.1)
|
|
38
37
|
addressable (~> 2.8)
|
|
39
|
-
base64 (~> 0.1)
|
|
40
38
|
http-cookie (~> 1.0)
|
|
41
39
|
http-form_data (~> 2.2)
|
|
42
40
|
llhttp-ffi (~> 0.5.0)
|
|
43
|
-
http-cookie (1.0
|
|
41
|
+
http-cookie (1.1.0)
|
|
44
42
|
domain_name (~> 0.5)
|
|
45
43
|
http-form_data (2.3.0)
|
|
46
|
-
json (2.
|
|
47
|
-
language_server-protocol (3.17.0.
|
|
44
|
+
json (2.16.0)
|
|
45
|
+
language_server-protocol (3.17.0.5)
|
|
48
46
|
lint_roller (1.1.0)
|
|
49
47
|
llhttp-ffi (0.5.1)
|
|
50
48
|
ffi-compiler (~> 1.0)
|
|
51
49
|
rake (~> 13.0)
|
|
52
|
-
maxmind-db (1.
|
|
53
|
-
minitest (5.
|
|
50
|
+
maxmind-db (1.4.0)
|
|
51
|
+
minitest (5.26.2)
|
|
54
52
|
parallel (1.27.0)
|
|
55
|
-
parser (3.3.
|
|
53
|
+
parser (3.3.10.0)
|
|
56
54
|
ast (~> 2.4.1)
|
|
57
55
|
racc
|
|
58
|
-
prism (1.
|
|
56
|
+
prism (1.6.0)
|
|
59
57
|
public_suffix (6.0.2)
|
|
60
58
|
racc (1.8.1)
|
|
61
59
|
rainbow (3.1.1)
|
|
62
|
-
rake (13.
|
|
63
|
-
regexp_parser (2.
|
|
64
|
-
rexml (3.4.
|
|
65
|
-
rubocop (1.
|
|
60
|
+
rake (13.3.1)
|
|
61
|
+
regexp_parser (2.11.3)
|
|
62
|
+
rexml (3.4.4)
|
|
63
|
+
rubocop (1.81.7)
|
|
66
64
|
json (~> 2.3)
|
|
67
65
|
language_server-protocol (~> 3.17.0.2)
|
|
68
66
|
lint_roller (~> 1.1.0)
|
|
@@ -70,21 +68,32 @@ GEM
|
|
|
70
68
|
parser (>= 3.3.0.2)
|
|
71
69
|
rainbow (>= 2.2.2, < 4.0)
|
|
72
70
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
73
|
-
rubocop-ast (>= 1.
|
|
71
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
74
72
|
ruby-progressbar (~> 1.7)
|
|
75
73
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
76
|
-
rubocop-ast (1.
|
|
74
|
+
rubocop-ast (1.48.0)
|
|
77
75
|
parser (>= 3.3.7.2)
|
|
78
76
|
prism (~> 1.4)
|
|
79
|
-
rubocop-
|
|
77
|
+
rubocop-minitest (0.38.2)
|
|
80
78
|
lint_roller (~> 1.1)
|
|
81
79
|
rubocop (>= 1.75.0, < 2.0)
|
|
82
80
|
rubocop-ast (>= 1.38.0, < 2.0)
|
|
81
|
+
rubocop-performance (1.26.1)
|
|
82
|
+
lint_roller (~> 1.1)
|
|
83
|
+
rubocop (>= 1.75.0, < 2.0)
|
|
84
|
+
rubocop-ast (>= 1.47.1, < 2.0)
|
|
85
|
+
rubocop-rake (0.7.1)
|
|
86
|
+
lint_roller (~> 1.1)
|
|
87
|
+
rubocop (>= 1.72.1)
|
|
88
|
+
rubocop-thread_safety (0.7.3)
|
|
89
|
+
lint_roller (~> 1.1)
|
|
90
|
+
rubocop (~> 1.72, >= 1.72.1)
|
|
91
|
+
rubocop-ast (>= 1.44.0, < 2.0)
|
|
83
92
|
ruby-progressbar (1.13.0)
|
|
84
|
-
unicode-display_width (3.
|
|
85
|
-
unicode-emoji (~> 4.
|
|
86
|
-
unicode-emoji (4.0
|
|
87
|
-
webmock (3.
|
|
93
|
+
unicode-display_width (3.2.0)
|
|
94
|
+
unicode-emoji (~> 4.1)
|
|
95
|
+
unicode-emoji (4.1.0)
|
|
96
|
+
webmock (3.26.1)
|
|
88
97
|
addressable (>= 2.8.0)
|
|
89
98
|
crack (>= 0.3.2)
|
|
90
99
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
@@ -107,8 +116,11 @@ DEPENDENCIES
|
|
|
107
116
|
minitest
|
|
108
117
|
rake
|
|
109
118
|
rubocop
|
|
119
|
+
rubocop-minitest
|
|
110
120
|
rubocop-performance
|
|
121
|
+
rubocop-rake
|
|
122
|
+
rubocop-thread_safety
|
|
111
123
|
webmock
|
|
112
124
|
|
|
113
125
|
BUNDLED WITH
|
|
114
|
-
2.6.
|
|
126
|
+
2.6.9
|
data/README.md
CHANGED
data/Rakefile
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'maxmind/geoip2/model/city'
|
|
4
|
+
require 'maxmind/geoip2/record/anonymizer'
|
|
4
5
|
|
|
5
6
|
module MaxMind
|
|
6
7
|
module GeoIP2
|
|
@@ -10,6 +11,19 @@ module MaxMind
|
|
|
10
11
|
# See https://dev.maxmind.com/geoip/docs/web-services?lang=en for more
|
|
11
12
|
# details.
|
|
12
13
|
class Insights < City
|
|
14
|
+
# Data indicating whether the IP address is part of an anonymizing
|
|
15
|
+
# network.
|
|
16
|
+
#
|
|
17
|
+
# @return [MaxMind::GeoIP2::Record::Anonymizer]
|
|
18
|
+
attr_reader :anonymizer
|
|
19
|
+
|
|
20
|
+
# @!visibility private
|
|
21
|
+
def initialize(record, locales)
|
|
22
|
+
super
|
|
23
|
+
@anonymizer = MaxMind::GeoIP2::Record::Anonymizer.new(
|
|
24
|
+
record['anonymizer'],
|
|
25
|
+
)
|
|
26
|
+
end
|
|
13
27
|
end
|
|
14
28
|
end
|
|
15
29
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'maxmind/geoip2/record/abstract'
|
|
5
|
+
|
|
6
|
+
module MaxMind
|
|
7
|
+
module GeoIP2
|
|
8
|
+
module Record
|
|
9
|
+
# Contains data indicating whether an IP address is part of an
|
|
10
|
+
# anonymizing network.
|
|
11
|
+
#
|
|
12
|
+
# This record is returned by the Insights web service.
|
|
13
|
+
class Anonymizer < Abstract
|
|
14
|
+
# A score ranging from 1 to 99 that represents our percent confidence
|
|
15
|
+
# that the network is currently part of an actively used VPN service.
|
|
16
|
+
# This property is only available from Insights.
|
|
17
|
+
#
|
|
18
|
+
# @return [Integer, nil]
|
|
19
|
+
def confidence
|
|
20
|
+
get('confidence')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# This is true if the IP address belongs to any sort of anonymous
|
|
24
|
+
# network. This property is only available from Insights.
|
|
25
|
+
#
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def anonymous?
|
|
28
|
+
get('is_anonymous')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# This is true if the IP address is registered to an anonymous VPN
|
|
32
|
+
# provider. If a VPN provider does not register subnets under names
|
|
33
|
+
# associated with them, we will likely only flag their IP ranges using
|
|
34
|
+
# the hosting_provider? property. This property is only available from
|
|
35
|
+
# Insights.
|
|
36
|
+
#
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def anonymous_vpn?
|
|
39
|
+
get('is_anonymous_vpn')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# This is true if the IP address belongs to a hosting or VPN provider
|
|
43
|
+
# (see description of the anonymous_vpn? property). This property is
|
|
44
|
+
# only available from Insights.
|
|
45
|
+
#
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def hosting_provider?
|
|
48
|
+
get('is_hosting_provider')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# This is true if the IP address belongs to a public proxy. This
|
|
52
|
+
# property is only available from Insights.
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def public_proxy?
|
|
56
|
+
get('is_public_proxy')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# This is true if the IP address is on a suspected anonymizing network
|
|
60
|
+
# and belongs to a residential ISP. This property is only available from
|
|
61
|
+
# Insights.
|
|
62
|
+
#
|
|
63
|
+
# @return [Boolean]
|
|
64
|
+
def residential_proxy?
|
|
65
|
+
get('is_residential_proxy')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# This is true if the IP address is a Tor exit node. This property is
|
|
69
|
+
# only available from Insights.
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean]
|
|
72
|
+
def tor_exit_node?
|
|
73
|
+
get('is_tor_exit_node')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The last day that the network was sighted in our analysis of
|
|
77
|
+
# anonymized networks. This value is parsed lazily. This property is
|
|
78
|
+
# only available from Insights.
|
|
79
|
+
#
|
|
80
|
+
# @return [Date, nil] A Date object representing the last seen date,
|
|
81
|
+
# or nil if the date is not available.
|
|
82
|
+
def network_last_seen
|
|
83
|
+
return @network_last_seen if defined?(@network_last_seen)
|
|
84
|
+
|
|
85
|
+
date_string = get('network_last_seen')
|
|
86
|
+
|
|
87
|
+
if !date_string
|
|
88
|
+
return nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@network_last_seen = Date.parse(date_string)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The name of the VPN provider (e.g., NordVPN, SurfShark, etc.)
|
|
95
|
+
# associated with the network. This property is only available from
|
|
96
|
+
# Insights.
|
|
97
|
+
#
|
|
98
|
+
# @return [String, nil]
|
|
99
|
+
def provider_name
|
|
100
|
+
get('provider_name')
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -76,7 +76,11 @@ module MaxMind
|
|
|
76
76
|
# This is true if the IP address belongs to any sort of anonymous network.
|
|
77
77
|
# This property is only available from Insights.
|
|
78
78
|
#
|
|
79
|
+
# This method is deprecated as of version 1.4.0. Use the anonymizer object
|
|
80
|
+
# from the Insights response instead.
|
|
81
|
+
#
|
|
79
82
|
# @return [Boolean]
|
|
83
|
+
# @deprecated since 1.4.0
|
|
80
84
|
def anonymous?
|
|
81
85
|
get('is_anonymous')
|
|
82
86
|
end
|
|
@@ -86,7 +90,11 @@ module MaxMind
|
|
|
86
90
|
# associated with them, we will likely only flag their IP ranges using the
|
|
87
91
|
# hosting_provider? property. This property is only available from Insights.
|
|
88
92
|
#
|
|
93
|
+
# This method is deprecated as of version 1.4.0. Use the anonymizer object
|
|
94
|
+
# from the Insights response instead.
|
|
95
|
+
#
|
|
89
96
|
# @return [Boolean]
|
|
97
|
+
# @deprecated since 1.4.0
|
|
90
98
|
def anonymous_vpn?
|
|
91
99
|
get('is_anonymous_vpn')
|
|
92
100
|
end
|
|
@@ -107,7 +115,11 @@ module MaxMind
|
|
|
107
115
|
# description of the anonymous_vpn? property). This property is only
|
|
108
116
|
# available from Insights.
|
|
109
117
|
#
|
|
118
|
+
# This method is deprecated as of version 1.4.0. Use the anonymizer object
|
|
119
|
+
# from the Insights response instead.
|
|
120
|
+
#
|
|
110
121
|
# @return [Boolean]
|
|
122
|
+
# @deprecated since 1.4.0
|
|
111
123
|
def hosting_provider?
|
|
112
124
|
get('is_hosting_provider')
|
|
113
125
|
end
|
|
@@ -146,7 +158,11 @@ module MaxMind
|
|
|
146
158
|
# This is true if the IP address belongs to a public proxy. This property
|
|
147
159
|
# is only available from Insights.
|
|
148
160
|
#
|
|
161
|
+
# This method is deprecated as of version 1.4.0. Use the anonymizer object
|
|
162
|
+
# from the Insights response instead.
|
|
163
|
+
#
|
|
149
164
|
# @return [Boolean]
|
|
165
|
+
# @deprecated since 1.4.0
|
|
150
166
|
def public_proxy?
|
|
151
167
|
get('is_public_proxy')
|
|
152
168
|
end
|
|
@@ -155,7 +171,11 @@ module MaxMind
|
|
|
155
171
|
# and belongs to a residential ISP. This property is only available
|
|
156
172
|
# from Insights.
|
|
157
173
|
#
|
|
174
|
+
# This method is deprecated as of version 1.4.0. Use the anonymizer object
|
|
175
|
+
# from the Insights response instead.
|
|
176
|
+
#
|
|
158
177
|
# @return [Boolean]
|
|
178
|
+
# @deprecated since 1.4.0
|
|
159
179
|
def residential_proxy?
|
|
160
180
|
get('is_residential_proxy')
|
|
161
181
|
end
|
|
@@ -163,7 +183,11 @@ module MaxMind
|
|
|
163
183
|
# This is true if the IP address is a Tor exit node. This property is only
|
|
164
184
|
# available from Insights.
|
|
165
185
|
#
|
|
186
|
+
# This method is deprecated as of version 1.4.0. Use the anonymizer object
|
|
187
|
+
# from the Insights response instead.
|
|
188
|
+
#
|
|
166
189
|
# @return [Boolean]
|
|
190
|
+
# @deprecated since 1.4.0
|
|
167
191
|
def tor_exit_node?
|
|
168
192
|
get('is_tor_exit_node')
|
|
169
193
|
end
|
|
@@ -212,6 +236,19 @@ module MaxMind
|
|
|
212
236
|
get('user_count')
|
|
213
237
|
end
|
|
214
238
|
|
|
239
|
+
# This field contains the risk associated with the IP address. The value
|
|
240
|
+
# ranges from 0.01 to 99. A higher score indicates a higher risk.
|
|
241
|
+
# Please note that the IP risk score provided in GeoIP products and
|
|
242
|
+
# services is more static than the IP risk score provided in minFraud
|
|
243
|
+
# and is not responsive to traffic on your network. If you need realtime
|
|
244
|
+
# IP risk scoring based on behavioral signals on your own network, please
|
|
245
|
+
# use minFraud. This property is only available from Insights.
|
|
246
|
+
#
|
|
247
|
+
# @return [Float, nil]
|
|
248
|
+
def ip_risk_snapshot
|
|
249
|
+
get('ip_risk_snapshot')
|
|
250
|
+
end
|
|
251
|
+
|
|
215
252
|
# The user type associated with the IP address. This can be one of the
|
|
216
253
|
# following values:
|
|
217
254
|
#
|
data/maxmind-geoip2.gemspec
CHANGED
|
@@ -24,15 +24,18 @@ Gem::Specification.new do |s|
|
|
|
24
24
|
'rubygems_mfa_required' => 'true',
|
|
25
25
|
'source_code_uri' => 'https://github.com/maxmind/GeoIP2-ruby',
|
|
26
26
|
}
|
|
27
|
-
s.required_ruby_version = '>= 3.
|
|
27
|
+
s.required_ruby_version = '>= 3.2'
|
|
28
28
|
|
|
29
29
|
s.add_dependency 'connection_pool', ['~> 2.2']
|
|
30
30
|
s.add_dependency 'http', '>= 4.3', '< 6.0'
|
|
31
|
-
s.add_dependency 'maxmind-db', ['~> 1.
|
|
31
|
+
s.add_dependency 'maxmind-db', ['~> 1.4']
|
|
32
32
|
|
|
33
33
|
s.add_development_dependency 'minitest'
|
|
34
34
|
s.add_development_dependency 'rake'
|
|
35
35
|
s.add_development_dependency 'rubocop'
|
|
36
|
+
s.add_development_dependency 'rubocop-minitest'
|
|
36
37
|
s.add_development_dependency 'rubocop-performance'
|
|
38
|
+
s.add_development_dependency 'rubocop-rake'
|
|
39
|
+
s.add_development_dependency 'rubocop-thread_safety'
|
|
37
40
|
s.add_development_dependency 'webmock'
|
|
38
41
|
end
|
data/test/test_client.rb
CHANGED
|
@@ -28,6 +28,17 @@ class ClientTest < Minitest::Test
|
|
|
28
28
|
}.freeze
|
|
29
29
|
|
|
30
30
|
INSIGHTS = {
|
|
31
|
+
'anonymizer' => {
|
|
32
|
+
'confidence' => 85,
|
|
33
|
+
'is_anonymous' => true,
|
|
34
|
+
'is_anonymous_vpn' => true,
|
|
35
|
+
'is_hosting_provider' => false,
|
|
36
|
+
'is_public_proxy' => false,
|
|
37
|
+
'is_residential_proxy' => true,
|
|
38
|
+
'is_tor_exit_node' => false,
|
|
39
|
+
'network_last_seen' => '2025-10-15',
|
|
40
|
+
'provider_name' => 'NordVPN',
|
|
41
|
+
},
|
|
31
42
|
'continent' => {
|
|
32
43
|
'code' => 'NA',
|
|
33
44
|
'geoname_id' => 42,
|
|
@@ -43,6 +54,7 @@ class ClientTest < Minitest::Test
|
|
|
43
54
|
},
|
|
44
55
|
'traits' => {
|
|
45
56
|
'ip_address' => '1.2.3.40',
|
|
57
|
+
'ip_risk_snapshot' => 45.5,
|
|
46
58
|
'is_anycast' => true,
|
|
47
59
|
'is_residential_proxy' => true,
|
|
48
60
|
'network' => '1.2.3.0/24',
|
|
@@ -66,16 +78,16 @@ class ClientTest < Minitest::Test
|
|
|
66
78
|
assert_equal('North America', record.continent.name)
|
|
67
79
|
|
|
68
80
|
assert_equal(1, record.country.geoname_id)
|
|
69
|
-
|
|
81
|
+
refute(record.country.in_european_union?)
|
|
70
82
|
assert_equal('US', record.country.iso_code)
|
|
71
83
|
assert_equal({ 'en' => 'United States of America' }, record.country.names)
|
|
72
84
|
assert_equal('United States of America', record.country.name)
|
|
73
85
|
|
|
74
86
|
assert_equal(11, record.maxmind.queries_remaining)
|
|
75
87
|
|
|
76
|
-
|
|
88
|
+
refute(record.registered_country.in_european_union?)
|
|
77
89
|
|
|
78
|
-
|
|
90
|
+
assert(record.traits.anycast?)
|
|
79
91
|
assert_equal('1.2.3.0/24', record.traits.network)
|
|
80
92
|
end
|
|
81
93
|
|
|
@@ -86,11 +98,24 @@ class ClientTest < Minitest::Test
|
|
|
86
98
|
|
|
87
99
|
assert_equal(42, record.continent.geoname_id)
|
|
88
100
|
|
|
89
|
-
|
|
90
|
-
assert_equal(
|
|
101
|
+
# Test anonymizer object
|
|
102
|
+
assert_equal(85, record.anonymizer.confidence)
|
|
103
|
+
assert(record.anonymizer.anonymous?)
|
|
104
|
+
assert(record.anonymizer.anonymous_vpn?)
|
|
105
|
+
refute(record.anonymizer.hosting_provider?)
|
|
106
|
+
refute(record.anonymizer.public_proxy?)
|
|
107
|
+
assert(record.anonymizer.residential_proxy?)
|
|
108
|
+
refute(record.anonymizer.tor_exit_node?)
|
|
109
|
+
assert_equal(Date.parse('2025-10-15'), record.anonymizer.network_last_seen)
|
|
110
|
+
assert_equal('NordVPN', record.anonymizer.provider_name)
|
|
111
|
+
|
|
112
|
+
# Test traits
|
|
113
|
+
assert(record.traits.anycast?)
|
|
114
|
+
assert(record.traits.residential_proxy?)
|
|
91
115
|
assert_equal('1.2.3.0/24', record.traits.network)
|
|
92
|
-
|
|
116
|
+
assert_in_delta(1.3, record.traits.static_ip_score)
|
|
93
117
|
assert_equal(2, record.traits.user_count)
|
|
118
|
+
assert_in_delta(45.5, record.traits.ip_risk_snapshot)
|
|
94
119
|
end
|
|
95
120
|
|
|
96
121
|
def test_city
|
data/test/test_model_country.rb
CHANGED
|
@@ -29,20 +29,18 @@ class CountryModelTest < Minitest::Test
|
|
|
29
29
|
|
|
30
30
|
def test_objects
|
|
31
31
|
model = MaxMind::GeoIP2::Model::Country.new(RAW, ['en'])
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
model.registered_country
|
|
32
|
+
|
|
33
|
+
assert_instance_of(MaxMind::GeoIP2::Model::Country, model)
|
|
34
|
+
assert_instance_of(MaxMind::GeoIP2::Record::Continent, model.continent)
|
|
35
|
+
assert_instance_of(MaxMind::GeoIP2::Record::Country, model.country)
|
|
36
|
+
assert_instance_of(
|
|
37
|
+
MaxMind::GeoIP2::Record::Country, model.registered_country,
|
|
38
38
|
)
|
|
39
|
-
|
|
40
|
-
MaxMind::GeoIP2::Record::RepresentedCountry,
|
|
41
|
-
model.represented_country.class,
|
|
39
|
+
assert_instance_of(
|
|
40
|
+
MaxMind::GeoIP2::Record::RepresentedCountry, model.represented_country,
|
|
42
41
|
)
|
|
43
|
-
|
|
44
|
-
MaxMind::GeoIP2::Record::Traits,
|
|
45
|
-
model.traits.class,
|
|
42
|
+
assert_instance_of(
|
|
43
|
+
MaxMind::GeoIP2::Record::Traits, model.traits,
|
|
46
44
|
)
|
|
47
45
|
end
|
|
48
46
|
|
|
@@ -55,14 +53,14 @@ class CountryModelTest < Minitest::Test
|
|
|
55
53
|
assert_equal('North America', model.continent.name)
|
|
56
54
|
|
|
57
55
|
assert_equal(1, model.country.geoname_id)
|
|
58
|
-
|
|
56
|
+
refute(model.country.in_european_union?)
|
|
59
57
|
assert_equal('US', model.country.iso_code)
|
|
60
58
|
assert_equal({ 'en' => 'United States of America' }, model.country.names)
|
|
61
59
|
assert_equal('United States of America', model.country.name)
|
|
62
60
|
assert_nil(model.country.confidence)
|
|
63
61
|
|
|
64
62
|
assert_equal(2, model.registered_country.geoname_id)
|
|
65
|
-
|
|
63
|
+
assert(model.registered_country.in_european_union?)
|
|
66
64
|
assert_equal('DE', model.registered_country.iso_code)
|
|
67
65
|
assert_equal({ 'en' => 'Germany' }, model.registered_country.names)
|
|
68
66
|
assert_equal('Germany', model.registered_country.name)
|
|
@@ -91,6 +89,7 @@ class CountryModelTest < Minitest::Test
|
|
|
91
89
|
},
|
|
92
90
|
['en'],
|
|
93
91
|
)
|
|
92
|
+
|
|
94
93
|
assert_equal(42, model.continent.geoname_id)
|
|
95
94
|
end
|
|
96
95
|
end
|
data/test/test_model_names.rb
CHANGED
|
@@ -29,18 +29,21 @@ class ModelNameTest < Minitest::Test
|
|
|
29
29
|
|
|
30
30
|
def test_fallback
|
|
31
31
|
model = MaxMind::GeoIP2::Model::Country.new(RAW, %w[ru zh-CN en])
|
|
32
|
+
|
|
32
33
|
assert_equal('北美洲', model.continent.name)
|
|
33
34
|
assert_equal('объединяет государства', model.country.name)
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def test_two_fallbacks
|
|
37
38
|
model = MaxMind::GeoIP2::Model::Country.new(RAW, %w[ru jp])
|
|
39
|
+
|
|
38
40
|
assert_nil(model.continent.name)
|
|
39
41
|
assert_equal('объединяет государства', model.country.name)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def test_no_fallbacks
|
|
43
45
|
model = MaxMind::GeoIP2::Model::Country.new(RAW, %w[jp])
|
|
46
|
+
|
|
44
47
|
assert_nil(model.continent.name)
|
|
45
48
|
assert_nil(model.country.name)
|
|
46
49
|
end
|
data/test/test_reader.rb
CHANGED
|
@@ -13,12 +13,12 @@ class ReaderTest < Minitest::Test
|
|
|
13
13
|
ip = '1.2.0.1'
|
|
14
14
|
record = reader.anonymous_ip(ip)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
assert(record.anonymous?)
|
|
17
|
+
assert(record.anonymous_vpn?)
|
|
18
|
+
refute(record.hosting_provider?)
|
|
19
|
+
refute(record.public_proxy?)
|
|
20
|
+
refute(record.residential_proxy?)
|
|
21
|
+
refute(record.tor_exit_node?)
|
|
22
22
|
assert_equal(ip, record.ip_address)
|
|
23
23
|
assert_equal('1.2.0.0/16', record.network)
|
|
24
24
|
|
|
@@ -32,7 +32,7 @@ class ReaderTest < Minitest::Test
|
|
|
32
32
|
ip = '81.2.69.1'
|
|
33
33
|
record = reader.anonymous_ip(ip)
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
assert(record.residential_proxy?)
|
|
36
36
|
|
|
37
37
|
reader.close
|
|
38
38
|
end
|
|
@@ -45,14 +45,14 @@ class ReaderTest < Minitest::Test
|
|
|
45
45
|
record = reader.anonymous_plus(ip)
|
|
46
46
|
|
|
47
47
|
assert_equal(30, record.anonymizer_confidence)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
assert(record.anonymous?)
|
|
49
|
+
assert(record.anonymous_vpn?)
|
|
50
|
+
refute(record.hosting_provider?)
|
|
51
51
|
assert_equal(Date.new(2025, 4, 14), record.network_last_seen)
|
|
52
52
|
assert_equal('foo', record.provider_name)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
refute(record.public_proxy?)
|
|
54
|
+
refute(record.residential_proxy?)
|
|
55
|
+
refute(record.tor_exit_node?)
|
|
56
56
|
|
|
57
57
|
assert_equal(ip, record.ip_address)
|
|
58
58
|
assert_equal('1.2.0.1/32', record.network)
|
|
@@ -89,8 +89,8 @@ class ReaderTest < Minitest::Test
|
|
|
89
89
|
assert_nil(record.city.confidence)
|
|
90
90
|
|
|
91
91
|
assert_equal(100, record.location.accuracy_radius)
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
assert_in_delta(51.75, record.location.latitude)
|
|
93
|
+
assert_in_delta(-1.25, record.location.longitude)
|
|
94
94
|
assert_equal('Europe/London', record.location.time_zone)
|
|
95
95
|
|
|
96
96
|
assert_equal(2, record.subdivisions.size)
|
|
@@ -117,14 +117,16 @@ class ReaderTest < Minitest::Test
|
|
|
117
117
|
'zh-CN' => '华盛顿州',
|
|
118
118
|
},
|
|
119
119
|
record.subdivisions[0].names,
|
|
120
|
-
assert_equal('WA', record.most_specific_subdivision.iso_code)
|
|
121
120
|
)
|
|
122
121
|
|
|
122
|
+
assert_equal('WA', record.most_specific_subdivision.iso_code)
|
|
123
|
+
|
|
123
124
|
# This IP has is_anycast.
|
|
124
125
|
|
|
125
126
|
ip_address = '214.1.1.0'
|
|
126
127
|
record = reader.city(ip_address)
|
|
127
|
-
|
|
128
|
+
|
|
129
|
+
assert(record.traits.anycast?)
|
|
128
130
|
|
|
129
131
|
reader.close
|
|
130
132
|
end
|
|
@@ -135,7 +137,7 @@ class ReaderTest < Minitest::Test
|
|
|
135
137
|
)
|
|
136
138
|
record = reader.city('2001:218::')
|
|
137
139
|
|
|
138
|
-
|
|
140
|
+
assert_empty(record.subdivisions)
|
|
139
141
|
assert_nil(record.most_specific_subdivision)
|
|
140
142
|
|
|
141
143
|
reader.close
|
|
@@ -179,7 +181,7 @@ class ReaderTest < Minitest::Test
|
|
|
179
181
|
assert_equal('Europe', record.continent.name)
|
|
180
182
|
|
|
181
183
|
assert_equal(2_635_167, record.country.geoname_id)
|
|
182
|
-
|
|
184
|
+
refute(record.country.in_european_union?)
|
|
183
185
|
assert_equal('GB', record.country.iso_code)
|
|
184
186
|
assert_equal(
|
|
185
187
|
{
|
|
@@ -197,7 +199,7 @@ class ReaderTest < Minitest::Test
|
|
|
197
199
|
assert_equal('United Kingdom', record.country.name)
|
|
198
200
|
|
|
199
201
|
assert_equal(3_017_382, record.registered_country.geoname_id)
|
|
200
|
-
|
|
202
|
+
assert(record.registered_country.in_european_union?)
|
|
201
203
|
assert_equal('FR', record.registered_country.iso_code)
|
|
202
204
|
assert_equal(
|
|
203
205
|
{
|
|
@@ -235,6 +237,7 @@ class ReaderTest < Minitest::Test
|
|
|
235
237
|
assert_equal('military', record.represented_country.type)
|
|
236
238
|
|
|
237
239
|
record = reader.country('81.2.69.163')
|
|
240
|
+
|
|
238
241
|
assert_equal('81.2.69.163', record.traits.ip_address)
|
|
239
242
|
assert_equal('81.2.69.160/27', record.traits.network)
|
|
240
243
|
|
|
@@ -242,7 +245,8 @@ class ReaderTest < Minitest::Test
|
|
|
242
245
|
|
|
243
246
|
ip_address = '214.1.1.0'
|
|
244
247
|
record = reader.country(ip_address)
|
|
245
|
-
|
|
248
|
+
|
|
249
|
+
assert(record.traits.anycast?)
|
|
246
250
|
|
|
247
251
|
assert_raises(NoMethodError) { record.foo }
|
|
248
252
|
reader.close
|
|
@@ -253,7 +257,8 @@ class ReaderTest < Minitest::Test
|
|
|
253
257
|
'test/data/test-data/GeoIP2-Country-Test.mmdb',
|
|
254
258
|
)
|
|
255
259
|
record = reader.country('74.209.24.0')
|
|
256
|
-
|
|
260
|
+
|
|
261
|
+
refute(record.country.in_european_union?)
|
|
257
262
|
|
|
258
263
|
reader.close
|
|
259
264
|
end
|
|
@@ -289,14 +294,14 @@ class ReaderTest < Minitest::Test
|
|
|
289
294
|
assert_equal(11, record.city.confidence)
|
|
290
295
|
assert_equal(99, record.country.confidence)
|
|
291
296
|
assert_equal(6_252_001, record.country.geoname_id)
|
|
292
|
-
|
|
297
|
+
refute(record.country.in_european_union?)
|
|
293
298
|
|
|
294
299
|
assert_equal(27, record.location.accuracy_radius)
|
|
295
300
|
|
|
296
|
-
|
|
301
|
+
refute(record.registered_country.in_european_union?)
|
|
297
302
|
|
|
298
303
|
assert_equal('Cable/DSL', record.traits.connection_type)
|
|
299
|
-
|
|
304
|
+
assert(record.traits.legitimate_proxy?)
|
|
300
305
|
|
|
301
306
|
assert_equal(ip_address, record.traits.ip_address)
|
|
302
307
|
assert_equal('74.209.16.0/20', record.traits.network)
|
|
@@ -313,7 +318,8 @@ class ReaderTest < Minitest::Test
|
|
|
313
318
|
|
|
314
319
|
ip_address = '214.1.1.0'
|
|
315
320
|
record = reader.enterprise(ip_address)
|
|
316
|
-
|
|
321
|
+
|
|
322
|
+
assert(record.traits.anycast?)
|
|
317
323
|
|
|
318
324
|
reader.close
|
|
319
325
|
end
|
|
@@ -352,7 +358,7 @@ class ReaderTest < Minitest::Test
|
|
|
352
358
|
assert_equal('2.125.160.216', record.traits.ip_address)
|
|
353
359
|
assert_equal('2.125.160.216/29', record.traits.network)
|
|
354
360
|
assert_nil(record.traits.autonomous_system_number)
|
|
355
|
-
|
|
361
|
+
refute(record.traits.anonymous?)
|
|
356
362
|
|
|
357
363
|
reader.close
|
|
358
364
|
end
|
|
@@ -438,6 +444,7 @@ class ReaderTest < Minitest::Test
|
|
|
438
444
|
"test/data/test-data/GeoIP2-#{t['file']}-Test.mmdb",
|
|
439
445
|
)
|
|
440
446
|
record = reader.send(t['method'], '81.2.69.160')
|
|
447
|
+
|
|
441
448
|
assert_equal('United Kingdom', record.country.name)
|
|
442
449
|
reader.close
|
|
443
450
|
end
|
|
@@ -450,6 +457,7 @@ class ReaderTest < Minitest::Test
|
|
|
450
457
|
%w[xx ru pt-BR es en],
|
|
451
458
|
)
|
|
452
459
|
record = reader.send(t['method'], '81.2.69.160')
|
|
460
|
+
|
|
453
461
|
assert_equal('Великобритания', record.country.name)
|
|
454
462
|
reader.close
|
|
455
463
|
end
|
|
@@ -461,6 +469,7 @@ class ReaderTest < Minitest::Test
|
|
|
461
469
|
"test/data/test-data/GeoIP2-#{t['file']}-Test.mmdb",
|
|
462
470
|
)
|
|
463
471
|
record = reader.send(t['method'], '81.2.69.163')
|
|
472
|
+
|
|
464
473
|
assert_equal('81.2.69.163', record.traits.ip_address)
|
|
465
474
|
assert_equal('81.2.69.160/27', record.traits.network)
|
|
466
475
|
reader.close
|
|
@@ -473,8 +482,9 @@ class ReaderTest < Minitest::Test
|
|
|
473
482
|
"test/data/test-data/GeoIP2-#{t['file']}-Test.mmdb",
|
|
474
483
|
)
|
|
475
484
|
record = reader.send(t['method'], '81.2.69.160')
|
|
476
|
-
|
|
477
|
-
|
|
485
|
+
|
|
486
|
+
refute(record.country.in_european_union?)
|
|
487
|
+
refute(record.registered_country.in_european_union?)
|
|
478
488
|
reader.close
|
|
479
489
|
end
|
|
480
490
|
end
|
|
@@ -524,6 +534,7 @@ class ReaderTest < Minitest::Test
|
|
|
524
534
|
reader = MaxMind::GeoIP2::Reader.new(
|
|
525
535
|
'test/data/test-data/GeoIP2-City-Test.mmdb',
|
|
526
536
|
)
|
|
537
|
+
|
|
527
538
|
assert_equal('GeoIP2-City', reader.metadata.database_type)
|
|
528
539
|
reader.close
|
|
529
540
|
end
|
|
@@ -533,6 +544,7 @@ class ReaderTest < Minitest::Test
|
|
|
533
544
|
database: 'test/data/test-data/GeoIP2-Country-Test.mmdb',
|
|
534
545
|
)
|
|
535
546
|
record = reader.country('81.2.69.160')
|
|
547
|
+
|
|
536
548
|
assert_equal('United Kingdom', record.country.name)
|
|
537
549
|
reader.close
|
|
538
550
|
end
|
|
@@ -544,6 +556,7 @@ class ReaderTest < Minitest::Test
|
|
|
544
556
|
mode: MaxMind::DB::MODE_MEMORY,
|
|
545
557
|
)
|
|
546
558
|
record = reader.country('81.2.69.160')
|
|
559
|
+
|
|
547
560
|
assert_equal('Великобритания', record.country.name)
|
|
548
561
|
reader.close
|
|
549
562
|
end
|
|
@@ -569,6 +582,7 @@ class ReaderTest < Minitest::Test
|
|
|
569
582
|
mode: MaxMind::DB::MODE_MEMORY,
|
|
570
583
|
)
|
|
571
584
|
record = reader.country('81.2.69.160')
|
|
585
|
+
|
|
572
586
|
assert_equal('Великобритания', record.country.name)
|
|
573
587
|
reader.close
|
|
574
588
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: maxmind-geoip2
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- William Storey
|
|
@@ -49,14 +49,14 @@ dependencies:
|
|
|
49
49
|
requirements:
|
|
50
50
|
- - "~>"
|
|
51
51
|
- !ruby/object:Gem::Version
|
|
52
|
-
version: '1.
|
|
52
|
+
version: '1.4'
|
|
53
53
|
type: :runtime
|
|
54
54
|
prerelease: false
|
|
55
55
|
version_requirements: !ruby/object:Gem::Requirement
|
|
56
56
|
requirements:
|
|
57
57
|
- - "~>"
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
|
-
version: '1.
|
|
59
|
+
version: '1.4'
|
|
60
60
|
- !ruby/object:Gem::Dependency
|
|
61
61
|
name: minitest
|
|
62
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -99,6 +99,20 @@ dependencies:
|
|
|
99
99
|
- - ">="
|
|
100
100
|
- !ruby/object:Gem::Version
|
|
101
101
|
version: '0'
|
|
102
|
+
- !ruby/object:Gem::Dependency
|
|
103
|
+
name: rubocop-minitest
|
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '0'
|
|
109
|
+
type: :development
|
|
110
|
+
prerelease: false
|
|
111
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
102
116
|
- !ruby/object:Gem::Dependency
|
|
103
117
|
name: rubocop-performance
|
|
104
118
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -113,6 +127,34 @@ dependencies:
|
|
|
113
127
|
- - ">="
|
|
114
128
|
- !ruby/object:Gem::Version
|
|
115
129
|
version: '0'
|
|
130
|
+
- !ruby/object:Gem::Dependency
|
|
131
|
+
name: rubocop-rake
|
|
132
|
+
requirement: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - ">="
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0'
|
|
137
|
+
type: :development
|
|
138
|
+
prerelease: false
|
|
139
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - ">="
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '0'
|
|
144
|
+
- !ruby/object:Gem::Dependency
|
|
145
|
+
name: rubocop-thread_safety
|
|
146
|
+
requirement: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - ">="
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '0'
|
|
151
|
+
type: :development
|
|
152
|
+
prerelease: false
|
|
153
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
154
|
+
requirements:
|
|
155
|
+
- - ">="
|
|
156
|
+
- !ruby/object:Gem::Version
|
|
157
|
+
version: '0'
|
|
116
158
|
- !ruby/object:Gem::Dependency
|
|
117
159
|
name: webmock
|
|
118
160
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -135,6 +177,7 @@ extensions: []
|
|
|
135
177
|
extra_rdoc_files: []
|
|
136
178
|
files:
|
|
137
179
|
- CHANGELOG.md
|
|
180
|
+
- CLAUDE.md
|
|
138
181
|
- Gemfile
|
|
139
182
|
- Gemfile.lock
|
|
140
183
|
- LICENSE-APACHE
|
|
@@ -158,6 +201,7 @@ files:
|
|
|
158
201
|
- lib/maxmind/geoip2/model/isp.rb
|
|
159
202
|
- lib/maxmind/geoip2/reader.rb
|
|
160
203
|
- lib/maxmind/geoip2/record/abstract.rb
|
|
204
|
+
- lib/maxmind/geoip2/record/anonymizer.rb
|
|
161
205
|
- lib/maxmind/geoip2/record/city.rb
|
|
162
206
|
- lib/maxmind/geoip2/record/continent.rb
|
|
163
207
|
- lib/maxmind/geoip2/record/country.rb
|
|
@@ -270,14 +314,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
270
314
|
requirements:
|
|
271
315
|
- - ">="
|
|
272
316
|
- !ruby/object:Gem::Version
|
|
273
|
-
version: '3.
|
|
317
|
+
version: '3.2'
|
|
274
318
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
275
319
|
requirements:
|
|
276
320
|
- - ">="
|
|
277
321
|
- !ruby/object:Gem::Version
|
|
278
322
|
version: '0'
|
|
279
323
|
requirements: []
|
|
280
|
-
rubygems_version: 3.6.
|
|
324
|
+
rubygems_version: 3.6.9
|
|
281
325
|
specification_version: 4
|
|
282
326
|
summary: A gem for interacting with the GeoIP2 webservices and databases.
|
|
283
327
|
test_files: []
|