trackdown 0.1.1 β 0.2.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/AGENTS.md +5 -0
- data/CHANGELOG.md +6 -0
- data/CLAUDE.md +5 -0
- data/README.md +149 -23
- data/Rakefile +10 -1
- data/lib/generators/trackdown/install_generator.rb +12 -4
- data/lib/generators/trackdown/templates/trackdown.rb +64 -11
- data/lib/trackdown/configuration.rb +24 -3
- data/lib/trackdown/ip_locator.rb +20 -61
- data/lib/trackdown/providers/auto_provider.rb +70 -0
- data/lib/trackdown/providers/base_provider.rb +39 -0
- data/lib/trackdown/providers/cloudflare_provider.rb +70 -0
- data/lib/trackdown/providers/maxmind_provider.rb +109 -0
- data/lib/trackdown/version.rb +1 -1
- data/lib/trackdown.rb +13 -3
- metadata +75 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ac258f067b151ee511c897d3460694466ed9ea36e2ff96010cccb5acf106b16
|
|
4
|
+
data.tar.gz: 1120ed37341b3fe876ed38997440537968e9014a1918b0c2c12acd3d10c36615
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd9029fb452c671613dcadb3081fc8e036634506f564b354997798d4b59617ac316c19976ae0ea2d9f4cfbc6a6d16a9a7729fc70d60d6a47354a808205bb6106
|
|
7
|
+
data.tar.gz: 94a789bbd321cb389590c466da2de5c7887812c9362d086a3d35446a428bd2a2a6a398f485d7629f86fec24e7e51a5b26d9871e512a4bb4e49f10edf9de45f7b
|
data/AGENTS.md
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.2.0] - 2026-01-02
|
|
2
|
+
|
|
3
|
+
- Completely decouple Maxmind from the gem, making it optional
|
|
4
|
+
- Add the provider pattern to support more Geo IP providers than MaxMind
|
|
5
|
+
- Add support for Cloudflare IP headers out of the box
|
|
6
|
+
|
|
1
7
|
## [0.1.1] - 2024-10-29
|
|
2
8
|
|
|
3
9
|
- Fix config validationerror on deployment
|
data/CLAUDE.md
ADDED
data/README.md
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
|
-
# π `trackdown` - Ruby gem to geolocate IPs
|
|
1
|
+
# π `trackdown` - Ruby gem to geolocate IPs
|
|
2
2
|
|
|
3
|
-
`trackdown` is a Ruby gem that
|
|
3
|
+
`trackdown` is a Ruby gem that allows you to geolocate IP addresses easily. It works out-of-the-box with **Cloudflare** (zero config!); and it's also a simple, convenient wrapper on top of **MaxMind** (just bring your own MaxMind key, and you're good to go!). `trackdown` offers a clean API for Rails applications to fetch country, city, and emoji flag information for any IP address.
|
|
4
4
|
|
|
5
5
|
Given an IP, it gives you the corresponding:
|
|
6
6
|
- πΊοΈ Country (two-letter country code + country name)
|
|
7
7
|
- π City
|
|
8
8
|
- πΊπΈ Emoji flag of the country
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Two ways to use `trackdown`
|
|
11
|
+
|
|
12
|
+
### Option 1: Cloudflare (recommended, zero config)
|
|
13
|
+
|
|
14
|
+
If your app is behind Cloudflare, you can use `trackdown` with **zero configuration**:
|
|
15
|
+
- No API keys needed
|
|
16
|
+
- No database downloads
|
|
17
|
+
- No external dependencies
|
|
18
|
+
- Instant lookups from Cloudflare headers
|
|
19
|
+
|
|
20
|
+
Just enable "IP Geolocation" in your Cloudflare dashboard and you're done! We automatically check for the Cloudflare headers in the context of a `request` and provide you with the IP geo data.
|
|
21
|
+
|
|
22
|
+
### Option 2: MaxMind (BYOK - Bring Your Own Key)
|
|
23
|
+
|
|
24
|
+
For apps not behind Cloudflare, offline apps, non-Rails apps, or as a fallback, use MaxMind:
|
|
25
|
+
- Requires MaxMind account and license key
|
|
26
|
+
- Requires downloading and maintaining a local database
|
|
27
|
+
- Works offline once database is downloaded
|
|
28
|
+
- Get started at [MaxMind](https://www.maxmind.com/)
|
|
29
|
+
|
|
30
|
+
### Option 3: Auto
|
|
31
|
+
|
|
32
|
+
By default, `trackdown` uses **`:auto` mode** which tries Cloudflare first and falls back to MaxMind automatically.
|
|
33
|
+
|
|
34
|
+
> [!NOTE]
|
|
35
|
+
> Trackdown fails gracefully. If no provider is available (no Cloudflare headers, no MaxMind database), it returns `'Unknown'` instead of raising an error, so your app doesn't crash due to a missing geolocation provider.
|
|
36
|
+
|
|
11
37
|
|
|
12
38
|
## Installation
|
|
13
39
|
|
|
@@ -15,6 +41,10 @@ Add this line to your application's Gemfile:
|
|
|
15
41
|
|
|
16
42
|
```ruby
|
|
17
43
|
gem 'trackdown'
|
|
44
|
+
|
|
45
|
+
# Optional: Only needed if using MaxMind provider
|
|
46
|
+
gem 'maxmind-db' # For MaxMind database access
|
|
47
|
+
gem 'connection_pool' # For connection pooling
|
|
18
48
|
```
|
|
19
49
|
|
|
20
50
|
And then execute:
|
|
@@ -25,79 +55,121 @@ bundle install
|
|
|
25
55
|
|
|
26
56
|
## Setup
|
|
27
57
|
|
|
28
|
-
|
|
58
|
+
### Quick Start (Cloudflare)
|
|
59
|
+
|
|
60
|
+
If your app is behind Cloudflare, setup is super simple:
|
|
61
|
+
|
|
62
|
+
1. **Enable IP Geolocation in Cloudflare**
|
|
29
63
|
|
|
64
|
+
2. **That's it!** No initializer needed. Just use it:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# In your controller
|
|
68
|
+
Trackdown.locate(request.remote_ip, request: request).country
|
|
69
|
+
# => 'United States'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Setup with MaxMind
|
|
73
|
+
|
|
74
|
+
If you want to use `trackdown` with a MaxMind database as the geo IP data provider:
|
|
75
|
+
|
|
76
|
+
1. **Run the generator**:
|
|
30
77
|
```bash
|
|
31
78
|
rails generate trackdown:install
|
|
32
79
|
```
|
|
33
80
|
|
|
34
|
-
This will create an initializer file at `config/initializers/trackdown.rb`. Open this file and add your MaxMind license key and account ID
|
|
81
|
+
This will create an initializer file at `config/initializers/trackdown.rb`. Open this file and add your MaxMind license key and account ID next.
|
|
35
82
|
|
|
83
|
+
2. **Configure your MaxMind credentials** in `config/initializers/trackdown.rb`:
|
|
36
84
|
```ruby
|
|
37
85
|
Trackdown.configure do |config|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
86
|
+
config.provider = :auto # or :maxmind to use MaxMind exclusively
|
|
87
|
+
|
|
88
|
+
# Use Rails credentials (recommended)
|
|
89
|
+
config.maxmind_account_id = Rails.application.credentials.dig(:maxmind, :account_id)
|
|
90
|
+
config.maxmind_license_key = Rails.application.credentials.dig(:maxmind, :license_key)
|
|
41
91
|
end
|
|
42
92
|
```
|
|
43
93
|
|
|
44
94
|
> [!TIP]
|
|
45
95
|
> To get your MaxMind account ID and license key, you need to create an account at [MaxMind](https://www.maxmind.com/) and get a license key.
|
|
46
96
|
|
|
47
|
-
|
|
97
|
+
3. **Download the database**:
|
|
98
|
+
```ruby
|
|
99
|
+
Trackdown.update_database
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
You can configure the path where the MaxMind database will be stored. By default, it will be stored at `db/GeoLite2-City.mmdb`:
|
|
48
103
|
|
|
49
104
|
```ruby
|
|
50
105
|
config.database_path = Rails.root.join('db', 'GeoLite2-City.mmdb').to_s
|
|
51
106
|
```
|
|
52
107
|
|
|
53
|
-
|
|
108
|
+
4. **Schedule regular updates** (optional but recommended):
|
|
109
|
+
|
|
110
|
+
The `trackdown` gem generator creates a `TrackdownDatabaseRefreshJob` job for regularly updating the MaxMind database. You can just get a database the first time and just keep using it, but the information will get outdated and some IPs will become stale or inaccurate.
|
|
54
111
|
|
|
55
112
|
To keep your IP geolocation accurate, you need to make sure the `TrackdownDatabaseRefreshJob` runs regularly. How you do that, exactly, depends on the queueing system you're using.
|
|
56
113
|
|
|
114
|
+
|
|
57
115
|
If you're using `solid_queue` (the Rails 8 default), you can easily add it to your schedule in the `config/recurring.yml` file like this:
|
|
116
|
+
|
|
58
117
|
```yaml
|
|
59
118
|
production:
|
|
60
|
-
|
|
119
|
+
refresh_trackdown_database:
|
|
61
120
|
class: TrackdownDatabaseRefreshJob
|
|
62
121
|
queue: default
|
|
63
|
-
schedule: every
|
|
122
|
+
schedule: every Saturday at 4am US/Pacific
|
|
64
123
|
```
|
|
65
124
|
|
|
66
|
-
|
|
125
|
+
> [!NOTE]
|
|
126
|
+
> MaxMind updates their databases [every Tuesday and Friday](https://dev.maxmind.com/geoip/geoip2/geoip2-update-process/).
|
|
127
|
+
|
|
128
|
+
## Usage
|
|
129
|
+
|
|
130
|
+
### With Cloudflare (recommended when available)
|
|
67
131
|
|
|
68
132
|
```ruby
|
|
69
|
-
|
|
133
|
+
# In your controller - pass the request object
|
|
134
|
+
result = Trackdown.locate(request.remote_ip, request: request)
|
|
135
|
+
result.country
|
|
136
|
+
# => 'United States'
|
|
70
137
|
```
|
|
71
138
|
|
|
72
|
-
|
|
139
|
+
### With MaxMind or without request object
|
|
73
140
|
|
|
74
141
|
To geolocate an IP address:
|
|
75
142
|
|
|
76
143
|
```ruby
|
|
77
|
-
|
|
144
|
+
# Works anywhere - just needs the IP
|
|
145
|
+
result = Trackdown.locate('8.8.8.8')
|
|
146
|
+
result.country
|
|
78
147
|
# => 'United States'
|
|
79
148
|
```
|
|
80
149
|
|
|
81
|
-
|
|
150
|
+
### API Methods
|
|
151
|
+
|
|
152
|
+
You can do things like:
|
|
82
153
|
```ruby
|
|
83
154
|
Trackdown.locate('8.8.8.8').emoji
|
|
84
155
|
# => 'πΊπΈ'
|
|
85
156
|
```
|
|
86
157
|
|
|
87
158
|
In fact, there are a few methods you can use:
|
|
88
|
-
```ruby
|
|
89
|
-
result = Trackdown.locate('8.8.8.8')
|
|
90
159
|
|
|
160
|
+
```ruby
|
|
91
161
|
result.country_code # => 'US'
|
|
92
162
|
result.country_name # => 'United States'
|
|
93
163
|
result.country # => 'United States' (alias for country_name)
|
|
94
|
-
result.
|
|
95
|
-
result.city # => 'Mountain View'
|
|
164
|
+
result.city # => 'Mountain View' (from MaxMind or Cloudflare's "Add visitor location headers")
|
|
96
165
|
result.flag_emoji # => 'πΊπΈ'
|
|
97
166
|
result.emoji # => 'πΊπΈ' (alias for flag_emoji)
|
|
98
167
|
result.country_flag # => 'πΊπΈ' (alias for flag_emoji)
|
|
168
|
+
result.country_info # => # Rich country data from the `countries` gem
|
|
99
169
|
```
|
|
100
170
|
|
|
171
|
+
### Rich country information
|
|
172
|
+
|
|
101
173
|
For `country_info` we're leveraging the [`countries`](https://github.com/countries/countries) gem, so you get a lot of information about the country, like the continent, the region, the languages spoken, the currency, and more:
|
|
102
174
|
|
|
103
175
|
```ruby
|
|
@@ -108,7 +180,10 @@ result.country_info.nationality # => 'American'
|
|
|
108
180
|
result.country_info.iso_long_name # => 'The United States of America'
|
|
109
181
|
```
|
|
110
182
|
|
|
183
|
+
### Hash data
|
|
184
|
+
|
|
111
185
|
If you prefer, you can also get all the information as a hash:
|
|
186
|
+
|
|
112
187
|
```ruby
|
|
113
188
|
result.to_h
|
|
114
189
|
# => {
|
|
@@ -120,15 +195,66 @@ result.to_h
|
|
|
120
195
|
# }
|
|
121
196
|
```
|
|
122
197
|
|
|
123
|
-
|
|
198
|
+
## Configuration
|
|
199
|
+
|
|
200
|
+
### Provider Options
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
Trackdown.configure do |config|
|
|
204
|
+
# :auto - Try Cloudflare first, fall back to MaxMind (default, recommended)
|
|
205
|
+
# :cloudflare - Only use Cloudflare headers
|
|
206
|
+
# :maxmind - Only use MaxMind database
|
|
207
|
+
config.provider = :auto
|
|
208
|
+
end
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Full Configuration Example
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
Trackdown.configure do |config|
|
|
215
|
+
# Provider
|
|
216
|
+
config.provider = :auto
|
|
217
|
+
|
|
218
|
+
# MaxMind settings (only needed if using MaxMind)
|
|
219
|
+
config.maxmind_account_id = Rails.application.credentials.dig(:maxmind, :account_id)
|
|
220
|
+
config.maxmind_license_key = Rails.application.credentials.dig(:maxmind, :license_key)
|
|
221
|
+
config.database_path = Rails.root.join('db', 'GeoLite2-City.mmdb').to_s
|
|
222
|
+
|
|
223
|
+
# Performance tuning (MaxMind only - requires maxmind-db gem)
|
|
224
|
+
config.timeout = 3
|
|
225
|
+
config.pool_size = 5
|
|
226
|
+
config.pool_timeout = 3
|
|
227
|
+
# config.memory_mode = MaxMind::DB::MODE_MEMORY # or MODE_FILE to reduce memory
|
|
228
|
+
|
|
229
|
+
# General
|
|
230
|
+
config.reject_private_ips = true # Reject 192.168.x.x, 127.0.0.1, etc.
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Updating the MaxMind database
|
|
235
|
+
|
|
236
|
+
Only needed when using the MaxMind provider:
|
|
237
|
+
|
|
124
238
|
```ruby
|
|
125
239
|
Trackdown.update_database
|
|
126
240
|
```
|
|
127
241
|
|
|
242
|
+
## How It Works
|
|
243
|
+
|
|
244
|
+
### Cloudflare Provider
|
|
245
|
+
|
|
246
|
+
When you enable "IP Geolocation" in Cloudflare, they add the `CF-IPCountry` header to every request. If you enable "Add visitor location headers" (via Managed Transforms), you also get `CF-IPCity`.
|
|
247
|
+
|
|
248
|
+
Trackdown reads these headers directly from the request with zero overhead, and no database lookups.
|
|
249
|
+
|
|
250
|
+
### MaxMind Provider
|
|
251
|
+
|
|
252
|
+
Downloads the GeoLite2-City database to your server and performs local lookups using connection pooling for performance.
|
|
253
|
+
|
|
128
254
|
|
|
129
255
|
## Development
|
|
130
256
|
|
|
131
|
-
After checking out the repo, run `
|
|
257
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake test` to run the Minitest tests.
|
|
132
258
|
|
|
133
259
|
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
134
260
|
|
data/Rakefile
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
|
-
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
t.verbose = true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
task default: :test
|
|
@@ -21,10 +21,18 @@ module Trackdown
|
|
|
21
21
|
|
|
22
22
|
def display_post_install_message
|
|
23
23
|
say "\tThe `trackdown` gem has been successfully installed!", :green
|
|
24
|
-
say "\
|
|
25
|
-
say " 1
|
|
26
|
-
say "
|
|
27
|
-
say "
|
|
24
|
+
say "\nChoose your setup path:"
|
|
25
|
+
say "\n Option 1: Cloudflare (Zero Config - Recommended)"
|
|
26
|
+
say " 1. Ensure your app is behind Cloudflare"
|
|
27
|
+
say " 2. Enable 'IP Geolocation' in Cloudflare dashboard (Network settings)"
|
|
28
|
+
say " 3. Use: Trackdown.locate(request.remote_ip, request: request)"
|
|
29
|
+
say " That's it! No API keys, no database needed."
|
|
30
|
+
say "\n Option 2: MaxMind (BYOK)"
|
|
31
|
+
say " 1. Configure your MaxMind credentials in `config/initializers/trackdown.rb`"
|
|
32
|
+
say " 2. Run 'Trackdown.update_database' to download the database"
|
|
33
|
+
say " 3. Schedule TrackdownDatabaseRefreshJob to run weekly"
|
|
34
|
+
say "\n Option 3: Auto (Best of Both)"
|
|
35
|
+
say " The default :auto mode tries Cloudflare first, falls back to MaxMind"
|
|
28
36
|
say "\nEnjoy `trackdown`!", :green
|
|
29
37
|
end
|
|
30
38
|
|
|
@@ -1,21 +1,74 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
Trackdown.configure do |config|
|
|
4
|
-
#
|
|
4
|
+
# ========================================
|
|
5
|
+
# Provider Selection
|
|
6
|
+
# ========================================
|
|
7
|
+
# Choose your IP geolocation provider:
|
|
8
|
+
#
|
|
9
|
+
# :auto (recommended, default)
|
|
10
|
+
# - Tries Cloudflare first (instant, zero overhead)
|
|
11
|
+
# - Falls back to MaxMind if Cloudflare not available
|
|
12
|
+
# - Perfect for hybrid deployments
|
|
13
|
+
#
|
|
14
|
+
# :cloudflare
|
|
15
|
+
# - Uses Cloudflare CF-IPCountry header
|
|
16
|
+
# - Requires: App behind Cloudflare + IP Geolocation enabled
|
|
17
|
+
# - Zero additional dependencies!
|
|
18
|
+
# - Must pass request object: Trackdown.locate(ip, request: request)
|
|
19
|
+
#
|
|
20
|
+
# :maxmind
|
|
21
|
+
# - Uses MaxMind GeoLite2 database
|
|
22
|
+
# - Requires: maxmind-db and connection_pool gems
|
|
23
|
+
# - Requires: MaxMind account and database download
|
|
24
|
+
#
|
|
25
|
+
config.provider = :auto
|
|
26
|
+
|
|
27
|
+
# ========================================
|
|
28
|
+
# Cloudflare Setup (for :cloudflare or :auto providers)
|
|
29
|
+
# ========================================
|
|
30
|
+
# 1. Ensure your app is behind Cloudflare
|
|
31
|
+
# 2. In Cloudflare dashboard β Network β Enable "IP Geolocation"
|
|
32
|
+
# OR under Rules β Transform Rules β Managed Transforms β Enable "Add visitor location headers"
|
|
33
|
+
# 3. Use: Trackdown.locate(request.remote_ip, request: request)
|
|
34
|
+
#
|
|
35
|
+
# That's it! No gems, no API keys, no database needed.
|
|
36
|
+
|
|
37
|
+
# ========================================
|
|
38
|
+
# MaxMind Setup (for :maxmind or :auto providers)
|
|
39
|
+
# ========================================
|
|
40
|
+
# Only needed if using MaxMind provider or as fallback
|
|
41
|
+
#
|
|
42
|
+
# 1. Add to Gemfile:
|
|
43
|
+
# gem 'maxmind-db'
|
|
44
|
+
# gem 'connection_pool'
|
|
45
|
+
#
|
|
46
|
+
# 2. Get your MaxMind account: https://www.maxmind.com/
|
|
47
|
+
#
|
|
48
|
+
# 3. Configure credentials (using Rails credentials recommended):
|
|
5
49
|
config.maxmind_account_id = Rails.application.credentials.dig(:maxmind, :account_id)
|
|
6
50
|
config.maxmind_license_key = Rails.application.credentials.dig(:maxmind, :license_key)
|
|
51
|
+
#
|
|
52
|
+
# 4. Run: Trackdown.update_database
|
|
53
|
+
#
|
|
54
|
+
# 5. Schedule regular updates (MaxMind updates Tue/Fri):
|
|
55
|
+
# Add to config/recurring.yml (for solid_queue):
|
|
56
|
+
# refresh_trackdown_database:
|
|
57
|
+
# class: TrackdownDatabaseRefreshJob
|
|
58
|
+
# schedule: every Saturday at 4am
|
|
7
59
|
|
|
8
|
-
# Optional:
|
|
60
|
+
# Optional: Database location (defaults to db/GeoLite2-City.mmdb)
|
|
9
61
|
# config.database_path = Rails.root.join('db', 'GeoLite2-City.mmdb').to_s
|
|
10
62
|
|
|
11
|
-
# Optional:
|
|
12
|
-
# config.timeout = 3 #
|
|
13
|
-
# config.pool_size = 5 #
|
|
14
|
-
# config.pool_timeout = 3 #
|
|
15
|
-
|
|
16
|
-
# Optional: Configure memory mode (defaults to MODE_MEMORY)
|
|
17
|
-
# config.memory_mode = MaxMind::DB::MODE_FILE # Use MODE_FILE to reduce memory usage
|
|
63
|
+
# Optional: MaxMind performance tuning
|
|
64
|
+
# config.timeout = 3 # Lookup timeout (seconds)
|
|
65
|
+
# config.pool_size = 5 # Connection pool size
|
|
66
|
+
# config.pool_timeout = 3 # Pool wait timeout (seconds)
|
|
67
|
+
# config.memory_mode = MaxMind::DB::MODE_MEMORY # or MODE_FILE to reduce memory
|
|
18
68
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
69
|
+
# ========================================
|
|
70
|
+
# General Options
|
|
71
|
+
# ========================================
|
|
72
|
+
# Reject private/local IP addresses (192.168.x.x, 127.0.0.1, etc.)
|
|
73
|
+
# config.reject_private_ips = true
|
|
21
74
|
end
|
|
@@ -1,25 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Conditionally require MaxMind constants if available
|
|
4
|
+
begin
|
|
5
|
+
require 'maxmind/db'
|
|
6
|
+
MAXMIND_AVAILABLE = true
|
|
7
|
+
rescue LoadError
|
|
8
|
+
MAXMIND_AVAILABLE = false
|
|
9
|
+
end
|
|
10
|
+
|
|
3
11
|
module Trackdown
|
|
4
12
|
class Configuration
|
|
5
|
-
attr_accessor :maxmind_license_key, :maxmind_account_id, :database_path,
|
|
13
|
+
attr_accessor :provider, :maxmind_license_key, :maxmind_account_id, :database_path,
|
|
6
14
|
:timeout, :pool_size, :pool_timeout, :memory_mode,
|
|
7
15
|
:reject_private_ips
|
|
8
16
|
|
|
17
|
+
# Available provider types:
|
|
18
|
+
# :auto - Try Cloudflare first, fall back to MaxMind (recommended)
|
|
19
|
+
# :cloudflare - Only use Cloudflare headers
|
|
20
|
+
# :maxmind - Only use MaxMind database
|
|
21
|
+
VALID_PROVIDERS = [:auto, :cloudflare, :maxmind].freeze
|
|
22
|
+
|
|
9
23
|
def initialize
|
|
24
|
+
@provider = :auto # Intelligent default: try Cloudflare first, fall back to MaxMind
|
|
10
25
|
@maxmind_license_key = nil
|
|
11
26
|
@maxmind_account_id = nil
|
|
12
27
|
@database_path = defined?(Rails) ? Rails.root.join('db', 'GeoLite2-City.mmdb').to_s : 'db/GeoLite2-City.mmdb'
|
|
13
28
|
@timeout = 3 # seconds
|
|
14
29
|
@pool_size = 5
|
|
15
30
|
@pool_timeout = 3 # seconds
|
|
16
|
-
@memory_mode = MaxMind::DB::MODE_MEMORY
|
|
31
|
+
@memory_mode = MAXMIND_AVAILABLE ? MaxMind::DB::MODE_MEMORY : nil
|
|
17
32
|
@reject_private_ips = true
|
|
18
33
|
end
|
|
19
34
|
|
|
35
|
+
def provider=(value)
|
|
36
|
+
unless VALID_PROVIDERS.include?(value)
|
|
37
|
+
raise ArgumentError, "Invalid provider: #{value}. Must be one of: #{VALID_PROVIDERS.join(', ')}"
|
|
38
|
+
end
|
|
39
|
+
@provider = value
|
|
40
|
+
end
|
|
41
|
+
|
|
20
42
|
def reject_private_ips?
|
|
21
43
|
@reject_private_ips
|
|
22
44
|
end
|
|
23
|
-
|
|
24
45
|
end
|
|
25
46
|
end
|
data/lib/trackdown/ip_locator.rb
CHANGED
|
@@ -1,83 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'maxmind/db'
|
|
4
|
-
require 'connection_pool'
|
|
5
3
|
require_relative 'location_result'
|
|
6
4
|
require_relative 'ip_validator'
|
|
5
|
+
require_relative 'providers/auto_provider'
|
|
6
|
+
require_relative 'providers/cloudflare_provider'
|
|
7
|
+
require_relative 'providers/maxmind_provider'
|
|
7
8
|
|
|
8
9
|
module Trackdown
|
|
9
10
|
class IpLocator
|
|
10
|
-
class TimeoutError < Trackdown::Error; end
|
|
11
|
-
class DatabaseError < Trackdown::Error; end
|
|
12
|
-
|
|
13
11
|
class << self
|
|
14
|
-
|
|
12
|
+
# Locate an IP address using the configured provider
|
|
13
|
+
# @param ip [String] The IP address to locate
|
|
14
|
+
# @param request [ActionDispatch::Request, nil] Optional Rails request object for Cloudflare provider
|
|
15
|
+
# @return [LocationResult] The location information
|
|
16
|
+
def locate(ip, request: nil)
|
|
15
17
|
IpValidator.validate!(ip)
|
|
16
18
|
|
|
17
19
|
if Trackdown.configuration.reject_private_ips? && IpValidator.private_ip?(ip)
|
|
18
20
|
raise IpValidator::InvalidIpError, "Private IP addresses are not allowed"
|
|
19
21
|
end
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
country_code = extract_country_code(record)
|
|
25
|
-
country_name = extract_country_name(record)
|
|
26
|
-
city = extract_city(record)
|
|
27
|
-
flag_emoji = get_emoji_flag(country_code)
|
|
28
|
-
|
|
29
|
-
LocationResult.new(country_code, country_name, city, flag_emoji)
|
|
23
|
+
provider = get_provider
|
|
24
|
+
provider.locate(ip, request: request)
|
|
30
25
|
end
|
|
31
26
|
|
|
32
27
|
private
|
|
33
28
|
|
|
34
|
-
def
|
|
35
|
-
Trackdown.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
def get_provider
|
|
30
|
+
case Trackdown.configuration.provider
|
|
31
|
+
when :auto
|
|
32
|
+
Providers::AutoProvider
|
|
33
|
+
when :cloudflare
|
|
34
|
+
Providers::CloudflareProvider
|
|
35
|
+
when :maxmind
|
|
36
|
+
Providers::MaxmindProvider
|
|
37
|
+
else
|
|
38
|
+
raise Trackdown::Error, "Unknown provider: #{Trackdown.configuration.provider}"
|
|
41
39
|
end
|
|
42
|
-
rescue Timeout::Error
|
|
43
|
-
raise TimeoutError, "MaxMind database lookup timed out after #{Trackdown.configuration.timeout} seconds"
|
|
44
|
-
rescue Trackdown::Error => e
|
|
45
|
-
raise e
|
|
46
|
-
rescue StandardError => e
|
|
47
|
-
Rails.logger.error("Error fetching IP data: #{e.message}") if defined?(Rails)
|
|
48
|
-
raise DatabaseError, "Database error: #{e.message}"
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def reader_pool
|
|
52
|
-
@reader_pool ||= ConnectionPool.new(
|
|
53
|
-
size: Trackdown.configuration.pool_size,
|
|
54
|
-
timeout: Trackdown.configuration.pool_timeout
|
|
55
|
-
) do
|
|
56
|
-
MaxMind::DB.new(
|
|
57
|
-
Trackdown.configuration.database_path,
|
|
58
|
-
mode: Trackdown.configuration.memory_mode
|
|
59
|
-
)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def extract_country_code(record)
|
|
64
|
-
record&.dig('country', 'iso_code')
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def extract_country_name(record)
|
|
68
|
-
record&.dig('country', 'names', 'en') ||
|
|
69
|
-
(record&.dig('country', 'names')&.values&.first) ||
|
|
70
|
-
'Unknown'
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def extract_city(record)
|
|
74
|
-
record&.dig('city', 'names', 'en') ||
|
|
75
|
-
(record&.dig('city', 'names')&.values&.first) ||
|
|
76
|
-
'Unknown'
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def get_emoji_flag(country_code)
|
|
80
|
-
country_code ? country_code.tr('A-Z', "\u{1F1E6}-\u{1F1FF}") : "π³οΈ"
|
|
81
40
|
end
|
|
82
41
|
end
|
|
83
42
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_provider'
|
|
4
|
+
require_relative 'cloudflare_provider'
|
|
5
|
+
require_relative 'maxmind_provider'
|
|
6
|
+
|
|
7
|
+
module Trackdown
|
|
8
|
+
module Providers
|
|
9
|
+
# Intelligent provider that automatically selects the best available provider
|
|
10
|
+
# Priority order:
|
|
11
|
+
# 1. Cloudflare (fastest, zero overhead, no external dependencies)
|
|
12
|
+
# 2. MaxMind (fallback when Cloudflare not available)
|
|
13
|
+
#
|
|
14
|
+
# This is the recommended default for most applications
|
|
15
|
+
class AutoProvider < BaseProvider
|
|
16
|
+
@@warned_no_providers = false
|
|
17
|
+
@@warn_mutex = Mutex.new
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Auto provider is available if at least one provider is available
|
|
21
|
+
def available?(request: nil)
|
|
22
|
+
CloudflareProvider.available?(request: request) ||
|
|
23
|
+
MaxmindProvider.available?(request: request)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Intelligently locate IP using the best available provider
|
|
27
|
+
# @param ip [String] The IP address to locate
|
|
28
|
+
# @param request [ActionDispatch::Request, nil] Optional Rails request object
|
|
29
|
+
# @return [LocationResult] The location information
|
|
30
|
+
def locate(ip, request: nil)
|
|
31
|
+
# Try Cloudflare first - it's instant and free!
|
|
32
|
+
if CloudflareProvider.available?(request: request)
|
|
33
|
+
return CloudflareProvider.locate(ip, request: request)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Fall back to MaxMind if available
|
|
37
|
+
if MaxmindProvider.available?(request: request)
|
|
38
|
+
return MaxmindProvider.locate(ip, request: request)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# No providers available - fail gracefully with a warning
|
|
42
|
+
warn_no_providers
|
|
43
|
+
LocationResult.new(nil, 'Unknown', 'Unknown', 'π³οΈ')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def warn_no_providers
|
|
49
|
+
# Only warn once per process to avoid log spam
|
|
50
|
+
return if @@warned_no_providers
|
|
51
|
+
|
|
52
|
+
@@warn_mutex.synchronize do
|
|
53
|
+
return if @@warned_no_providers
|
|
54
|
+
@@warned_no_providers = true
|
|
55
|
+
|
|
56
|
+
message = "[Trackdown] No IP geolocation provider available. Returning 'Unknown' for all lookups. " \
|
|
57
|
+
"Configure Cloudflare headers or MaxMind to enable geolocation. " \
|
|
58
|
+
"See: https://github.com/rameerez/trackdown"
|
|
59
|
+
|
|
60
|
+
if defined?(Rails)
|
|
61
|
+
Rails.logger.warn(message)
|
|
62
|
+
else
|
|
63
|
+
warn(message)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'countries'
|
|
4
|
+
|
|
5
|
+
module Trackdown
|
|
6
|
+
module Providers
|
|
7
|
+
class BaseProvider
|
|
8
|
+
# Returns true if this provider can handle the given request/context
|
|
9
|
+
def self.available?(request: nil)
|
|
10
|
+
raise NotImplementedError, "#{self} must implement .available?"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Locates the IP and returns a LocationResult
|
|
14
|
+
# @param ip [String] The IP address to locate
|
|
15
|
+
# @param request [ActionDispatch::Request, nil] Optional Rails request object for header access
|
|
16
|
+
# @return [LocationResult] The location information
|
|
17
|
+
def self.locate(ip, request: nil)
|
|
18
|
+
raise NotImplementedError, "#{self} must implement .locate"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
protected
|
|
22
|
+
|
|
23
|
+
# Helper to get emoji flag from country code
|
|
24
|
+
def self.get_emoji_flag(country_code)
|
|
25
|
+
country_code ? country_code.tr('A-Z', "\u{1F1E6}-\u{1F1FF}") : "π³οΈ"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Helper to extract country name from country code using countries gem
|
|
29
|
+
def self.get_country_name(country_code)
|
|
30
|
+
return 'Unknown' unless country_code
|
|
31
|
+
|
|
32
|
+
country = ISO3166::Country.new(country_code)
|
|
33
|
+
country&.iso_short_name || country&.name || 'Unknown'
|
|
34
|
+
rescue StandardError
|
|
35
|
+
'Unknown'
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_provider'
|
|
4
|
+
require_relative '../location_result'
|
|
5
|
+
|
|
6
|
+
module Trackdown
|
|
7
|
+
module Providers
|
|
8
|
+
# Provider that uses Cloudflare HTTP headers for IP geolocation
|
|
9
|
+
# This is the fastest and most lightweight option when your app is behind Cloudflare
|
|
10
|
+
#
|
|
11
|
+
# Cloudflare must have "IP Geolocation" or "Add visitor location headers" enabled
|
|
12
|
+
# in the dashboard under Network settings or via Managed Transforms
|
|
13
|
+
class CloudflareProvider < BaseProvider
|
|
14
|
+
COUNTRY_HEADER = 'HTTP_CF_IPCOUNTRY'
|
|
15
|
+
CITY_HEADER = 'HTTP_CF_IPCITY'
|
|
16
|
+
|
|
17
|
+
# Special Cloudflare country codes
|
|
18
|
+
UNKNOWN_CODE = 'XX'
|
|
19
|
+
TOR_CODE = 'T1'
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# Check if Cloudflare headers are available in the request
|
|
23
|
+
def available?(request: nil)
|
|
24
|
+
return false unless request
|
|
25
|
+
|
|
26
|
+
country_code = request.env[COUNTRY_HEADER]
|
|
27
|
+
!country_code.nil? && !country_code.empty? && country_code != UNKNOWN_CODE
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Locate IP using Cloudflare headers
|
|
31
|
+
# @param ip [String] The IP address (not used, as Cloudflare already resolved it)
|
|
32
|
+
# @param request [ActionDispatch::Request] Rails request object with Cloudflare headers
|
|
33
|
+
# @return [LocationResult] The location information
|
|
34
|
+
def locate(ip, request: nil)
|
|
35
|
+
raise Trackdown::Error, "CloudflareProvider requires a request object with Cloudflare headers" unless request
|
|
36
|
+
|
|
37
|
+
country_code = extract_country_code(request)
|
|
38
|
+
|
|
39
|
+
# If no valid country code, return unknown
|
|
40
|
+
return LocationResult.new(nil, 'Unknown', 'Unknown', 'π³οΈ') if country_code.nil? || country_code == UNKNOWN_CODE
|
|
41
|
+
|
|
42
|
+
country_name = get_country_name(country_code)
|
|
43
|
+
city = extract_city(request)
|
|
44
|
+
flag_emoji = get_emoji_flag(country_code)
|
|
45
|
+
|
|
46
|
+
LocationResult.new(country_code, country_name, city, flag_emoji)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def extract_country_code(request)
|
|
52
|
+
code = request.env[COUNTRY_HEADER]
|
|
53
|
+
return nil if code.nil? || code.empty? || code == UNKNOWN_CODE
|
|
54
|
+
|
|
55
|
+
code.upcase
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extract_city(request)
|
|
59
|
+
city = request.env[CITY_HEADER]
|
|
60
|
+
|
|
61
|
+
# Cloudflare city header might not always be present
|
|
62
|
+
# It requires "Add visitor location headers" Managed Transform
|
|
63
|
+
return 'Unknown' if city.nil? || city.empty?
|
|
64
|
+
|
|
65
|
+
city
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'timeout'
|
|
4
|
+
require_relative 'base_provider'
|
|
5
|
+
require_relative '../location_result'
|
|
6
|
+
|
|
7
|
+
# Conditionally require MaxMind - this is an optional dependency
|
|
8
|
+
begin
|
|
9
|
+
require 'maxmind/db'
|
|
10
|
+
require 'connection_pool'
|
|
11
|
+
rescue LoadError
|
|
12
|
+
# MaxMind gem not available - that's ok, other providers might be used
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Trackdown
|
|
16
|
+
module Providers
|
|
17
|
+
# Provider that uses MaxMind GeoLite2 database for IP geolocation
|
|
18
|
+
# Requires the maxmind-db gem and a downloaded database file
|
|
19
|
+
class MaxmindProvider < BaseProvider
|
|
20
|
+
class TimeoutError < Trackdown::Error; end
|
|
21
|
+
class DatabaseError < Trackdown::Error; end
|
|
22
|
+
|
|
23
|
+
@@reader_pool = nil
|
|
24
|
+
@@pool_mutex = Mutex.new
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Check if MaxMind database is available
|
|
28
|
+
def available?(request: nil)
|
|
29
|
+
return false unless maxmind_available?
|
|
30
|
+
return false unless Trackdown.database_exists?
|
|
31
|
+
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Locate IP using MaxMind database
|
|
36
|
+
# @param ip [String] The IP address to locate
|
|
37
|
+
# @param request [ActionDispatch::Request, nil] Not used by MaxMind provider
|
|
38
|
+
# @return [LocationResult] The location information
|
|
39
|
+
def locate(ip, request: nil)
|
|
40
|
+
raise Trackdown::Error, "MaxMind database not found" unless Trackdown.database_exists?
|
|
41
|
+
raise Trackdown::Error, "maxmind-db gem not installed. Add it to your Gemfile: gem 'maxmind-db'" unless maxmind_available?
|
|
42
|
+
|
|
43
|
+
record = fetch_record(ip)
|
|
44
|
+
return LocationResult.new(nil, 'Unknown', 'Unknown', 'π³οΈ') if record.nil?
|
|
45
|
+
|
|
46
|
+
country_code = extract_country_code(record)
|
|
47
|
+
country_name = extract_country_name(record)
|
|
48
|
+
city = extract_city(record)
|
|
49
|
+
flag_emoji = get_emoji_flag(country_code)
|
|
50
|
+
|
|
51
|
+
LocationResult.new(country_code, country_name, city, flag_emoji)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def maxmind_available?
|
|
57
|
+
defined?(MaxMind::DB)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def fetch_record(ip)
|
|
61
|
+
Timeout.timeout(Trackdown.configuration.timeout) do
|
|
62
|
+
reader_pool.with do |reader|
|
|
63
|
+
reader.get(ip)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
rescue Timeout::Error
|
|
67
|
+
raise TimeoutError, "MaxMind database lookup timed out after #{Trackdown.configuration.timeout} seconds"
|
|
68
|
+
rescue Trackdown::Error => e
|
|
69
|
+
raise e
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
Rails.logger.error("Error fetching IP data: #{e.message}") if defined?(Rails)
|
|
72
|
+
raise DatabaseError, "Database error: #{e.message}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def reader_pool
|
|
76
|
+
return @@reader_pool if @@reader_pool
|
|
77
|
+
|
|
78
|
+
@@pool_mutex.synchronize do
|
|
79
|
+
@@reader_pool ||= ConnectionPool.new(
|
|
80
|
+
size: Trackdown.configuration.pool_size,
|
|
81
|
+
timeout: Trackdown.configuration.pool_timeout
|
|
82
|
+
) do
|
|
83
|
+
MaxMind::DB.new(
|
|
84
|
+
Trackdown.configuration.database_path,
|
|
85
|
+
mode: Trackdown.configuration.memory_mode
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def extract_country_code(record)
|
|
92
|
+
record&.dig('country', 'iso_code')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def extract_country_name(record)
|
|
96
|
+
record&.dig('country', 'names', 'en') ||
|
|
97
|
+
(record&.dig('country', 'names')&.values&.first) ||
|
|
98
|
+
'Unknown'
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def extract_city(record)
|
|
102
|
+
record&.dig('city', 'names', 'en') ||
|
|
103
|
+
(record&.dig('city', 'names')&.values&.first) ||
|
|
104
|
+
'Unknown'
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/trackdown/version.rb
CHANGED
data/lib/trackdown.rb
CHANGED
|
@@ -7,6 +7,10 @@ require_relative "trackdown/ip_validator"
|
|
|
7
7
|
require_relative "trackdown/ip_locator"
|
|
8
8
|
require_relative "trackdown/database_updater"
|
|
9
9
|
require_relative "trackdown/location_result"
|
|
10
|
+
require_relative "trackdown/providers/base_provider"
|
|
11
|
+
require_relative "trackdown/providers/cloudflare_provider"
|
|
12
|
+
require_relative "trackdown/providers/maxmind_provider"
|
|
13
|
+
require_relative "trackdown/providers/auto_provider"
|
|
10
14
|
|
|
11
15
|
module Trackdown
|
|
12
16
|
class << self
|
|
@@ -21,11 +25,15 @@ module Trackdown
|
|
|
21
25
|
yield(configuration)
|
|
22
26
|
end
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
# Locate an IP address using the configured provider
|
|
29
|
+
# @param ip [String] The IP address to locate
|
|
30
|
+
# @param request [ActionDispatch::Request, nil] Optional Rails request object (required for Cloudflare provider)
|
|
31
|
+
# @return [LocationResult] The location information
|
|
32
|
+
def self.locate(ip, request: nil)
|
|
33
|
+
IpLocator.locate(ip, request: request)
|
|
27
34
|
end
|
|
28
35
|
|
|
36
|
+
# Update the MaxMind database (only needed when using MaxMind provider)
|
|
29
37
|
def self.update_database
|
|
30
38
|
DatabaseUpdater.update
|
|
31
39
|
end
|
|
@@ -34,6 +42,8 @@ module Trackdown
|
|
|
34
42
|
File.exist?(configuration.database_path)
|
|
35
43
|
end
|
|
36
44
|
|
|
45
|
+
# Legacy method - kept for backwards compatibility
|
|
46
|
+
# New code should handle provider-specific errors instead
|
|
37
47
|
def self.ensure_database_exists!
|
|
38
48
|
unless database_exists?
|
|
39
49
|
raise Error, "MaxMind database not found. Please set your MaxMind keys in config/initializers/trackdown.rb as described in the `trackdown` gem README, and then run Trackdown.update_database to download the MaxMind IP geolocation database."
|
metadata
CHANGED
|
@@ -1,73 +1,100 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trackdown
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 2026-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
13
|
+
name: countries
|
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
|
16
15
|
requirements:
|
|
17
16
|
- - "~>"
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
18
|
+
version: '7.0'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - "~>"
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
25
|
+
version: '7.0'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
27
|
+
name: rake
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
30
29
|
requirements:
|
|
31
30
|
- - "~>"
|
|
32
31
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
34
|
-
type: :
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
35
34
|
prerelease: false
|
|
36
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
36
|
requirements:
|
|
38
37
|
- - "~>"
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
39
|
+
version: '13.0'
|
|
41
40
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
41
|
+
name: rubocop
|
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
|
44
43
|
requirements:
|
|
45
44
|
- - "~>"
|
|
46
45
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
48
|
-
type: :
|
|
46
|
+
version: '1.21'
|
|
47
|
+
type: :development
|
|
49
48
|
prerelease: false
|
|
50
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
50
|
requirements:
|
|
52
51
|
- - "~>"
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
53
|
+
version: '1.21'
|
|
55
54
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
55
|
+
name: minitest
|
|
57
56
|
requirement: !ruby/object:Gem::Requirement
|
|
58
57
|
requirements:
|
|
59
58
|
- - "~>"
|
|
60
59
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
60
|
+
version: '5.25'
|
|
62
61
|
type: :development
|
|
63
62
|
prerelease: false
|
|
64
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
64
|
requirements:
|
|
66
65
|
- - "~>"
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
67
|
+
version: '5.25'
|
|
69
68
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
69
|
+
name: mocha
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '2.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '2.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: simplecov
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.22'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.22'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: webmock
|
|
71
98
|
requirement: !ruby/object:Gem::Requirement
|
|
72
99
|
requirements:
|
|
73
100
|
- - "~>"
|
|
@@ -81,31 +108,47 @@ dependencies:
|
|
|
81
108
|
- !ruby/object:Gem::Version
|
|
82
109
|
version: '3.0'
|
|
83
110
|
- !ruby/object:Gem::Dependency
|
|
84
|
-
name:
|
|
111
|
+
name: maxmind-db
|
|
85
112
|
requirement: !ruby/object:Gem::Requirement
|
|
86
113
|
requirements:
|
|
87
114
|
- - "~>"
|
|
88
115
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '1.
|
|
116
|
+
version: '1.2'
|
|
90
117
|
type: :development
|
|
91
118
|
prerelease: false
|
|
92
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
120
|
requirements:
|
|
94
121
|
- - "~>"
|
|
95
122
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '1.
|
|
123
|
+
version: '1.2'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: connection_pool
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '2.4'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '2.4'
|
|
97
138
|
description: Trackdown is a Ruby gem that easily allows you to geolocate IP addresses.
|
|
98
|
-
It
|
|
99
|
-
|
|
100
|
-
and
|
|
101
|
-
|
|
139
|
+
It works out of the box with Cloudflare headers if you're using it, or you can use
|
|
140
|
+
MaxMind (BYOK). The gem offers a clean API for Rails applications to fetch country,
|
|
141
|
+
city, and emoji flag information for any IP address. Supports Cloudflare headers
|
|
142
|
+
(instant, zero overhead) and MaxMind GeoLite2 database (offline capable).
|
|
102
143
|
email:
|
|
103
144
|
- rubygems@rameerez.com
|
|
104
145
|
executables: []
|
|
105
146
|
extensions: []
|
|
106
147
|
extra_rdoc_files: []
|
|
107
148
|
files:
|
|
149
|
+
- AGENTS.md
|
|
108
150
|
- CHANGELOG.md
|
|
151
|
+
- CLAUDE.md
|
|
109
152
|
- LICENSE.txt
|
|
110
153
|
- README.md
|
|
111
154
|
- Rakefile
|
|
@@ -119,6 +162,10 @@ files:
|
|
|
119
162
|
- lib/trackdown/ip_locator.rb
|
|
120
163
|
- lib/trackdown/ip_validator.rb
|
|
121
164
|
- lib/trackdown/location_result.rb
|
|
165
|
+
- lib/trackdown/providers/auto_provider.rb
|
|
166
|
+
- lib/trackdown/providers/base_provider.rb
|
|
167
|
+
- lib/trackdown/providers/cloudflare_provider.rb
|
|
168
|
+
- lib/trackdown/providers/maxmind_provider.rb
|
|
122
169
|
- lib/trackdown/version.rb
|
|
123
170
|
- sig/trackdown.rbs
|
|
124
171
|
homepage: https://github.com/rameerez/trackdown
|
|
@@ -129,7 +176,6 @@ metadata:
|
|
|
129
176
|
homepage_uri: https://github.com/rameerez/trackdown
|
|
130
177
|
source_code_uri: https://github.com/rameerez/trackdown
|
|
131
178
|
changelog_uri: https://github.com/rameerez/trackdown/blob/main/CHANGELOG.md
|
|
132
|
-
post_install_message:
|
|
133
179
|
rdoc_options: []
|
|
134
180
|
require_paths:
|
|
135
181
|
- lib
|
|
@@ -144,9 +190,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
144
190
|
- !ruby/object:Gem::Version
|
|
145
191
|
version: '0'
|
|
146
192
|
requirements: []
|
|
147
|
-
rubygems_version: 3.
|
|
148
|
-
signing_key:
|
|
193
|
+
rubygems_version: 3.6.2
|
|
149
194
|
specification_version: 4
|
|
150
|
-
summary: Get country, city, and emoji flag information for IP addresses using
|
|
151
|
-
|
|
195
|
+
summary: Get country, city, and emoji flag information for IP addresses using Cloudflare
|
|
196
|
+
or MaxMind
|
|
152
197
|
test_files: []
|