From 78ab6304ce4436b439a7e5712ee2fcc792105825 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 1 Aug 2025 15:48:05 +0200 Subject: [PATCH 1/8] Bump Ruby version to 3.2.2 --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index 37c2961..be94e6f 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.2 +3.2.2 From f5026750e3b2ed085dfe07215effc0faa7ae3e94 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 1 Aug 2025 15:57:11 +0200 Subject: [PATCH 2/8] Fix rubocop issues in gemspec --- ipinfo-rails.gemspec | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ipinfo-rails.gemspec b/ipinfo-rails.gemspec index 2b7897e..ddf3f03 100644 --- a/ipinfo-rails.gemspec +++ b/ipinfo-rails.gemspec @@ -9,7 +9,6 @@ Gem::Specification.new do |s| s.name = 'ipinfo-rails' s.version = IPinfoRails::VERSION s.required_ruby_version = '>= 2.5.0' - s.date = '2018-12-10' s.summary = 'The official Rails gem for IPinfo. IPinfo prides itself on ' \ 'being the most reliable, accurate, and in-depth source of ' \ 'IP address data available anywhere. We process terabytes ' \ @@ -29,4 +28,7 @@ Gem::Specification.new do |s| f.match(%r{^(test|spec|features)/}) end s.require_paths = ['lib'] + s.metadata = { + 'rubygems_mfa_required' => 'true' + } end From 93617392286bbd5dfa8bcba924825ab5dac0f972 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 1 Aug 2025 16:11:15 +0200 Subject: [PATCH 3/8] Add Rakefile to ease running commands --- Rakefile | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Rakefile diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..4789edb --- /dev/null +++ b/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'bundler/gem_tasks' +require 'rake/testtask' + +Rake::TestTask.new(:test) do |t| + t.libs << 'test' + t.libs << 'lib' + t.test_files = FileList['test/**/*_test.rb'] +end + +task default: :test From 35811b30a45bd95b4fcc4dfaa95b8f9c4a28481e Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 1 Aug 2025 16:11:29 +0200 Subject: [PATCH 4/8] Bump IPinfo dependency --- ipinfo-rails.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ipinfo-rails.gemspec b/ipinfo-rails.gemspec index ddf3f03..2ad9183 100644 --- a/ipinfo-rails.gemspec +++ b/ipinfo-rails.gemspec @@ -21,8 +21,8 @@ Gem::Specification.new do |s| s.homepage = 'https://ipinfo.io' s.license = 'Apache-2.0' - s.add_runtime_dependency 'IPinfo', '~> 1.0.1' - s.add_runtime_dependency 'rack', '~> 2.0' + s.add_dependency 'IPinfo', '~> 2.3' + s.add_dependency 'rack', '~> 2.0' s.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) From 5b9123ec2f810da0f6b6f5b462b68d4d34fbca72 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 1 Aug 2025 16:39:42 +0200 Subject: [PATCH 5/8] Add tests and workflow to run them --- .github/workflows/test.yml | 31 ++++++ ipinfo-rails.gemspec | 2 + test/ipinfo_middleware_test.rb | 181 +++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 test/ipinfo_middleware_test.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4691cac --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + run: + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ["3.2", "3.3", "3.4"] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Ruby ${{ matrix.ruby-version }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + + - name: Install dependencies + run: bundle install + + - name: Run tests + run: bundle exec rake diff --git a/ipinfo-rails.gemspec b/ipinfo-rails.gemspec index 2ad9183..f74f927 100644 --- a/ipinfo-rails.gemspec +++ b/ipinfo-rails.gemspec @@ -24,6 +24,8 @@ Gem::Specification.new do |s| s.add_dependency 'IPinfo', '~> 2.3' s.add_dependency 'rack', '~> 2.0' + s.add_development_dependency 'mocha', '~> 2.7' + s.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r{^(test|spec|features)/}) end diff --git a/test/ipinfo_middleware_test.rb b/test/ipinfo_middleware_test.rb new file mode 100644 index 0000000..bd9b7a5 --- /dev/null +++ b/test/ipinfo_middleware_test.rb @@ -0,0 +1,181 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'minitest/mock' +require 'mocha/minitest' +require 'rack/mock' +require 'ostruct' +require 'ipinfo' +require 'ipinfo/errors' +require_relative '../lib/ipinfo-rails' + + +# Simple Rack app +class TestApp + attr_reader :last_env + + def call(env) + @last_env = env + [200, { 'Content-Type' => 'text/plain' }, ['Hello from TestApp!']] + end +end + +class IPinfoMiddlewareTest < Minitest::Test + def setup + @app = TestApp.new + @middleware = nil + @mock_ipinfo_client = mock('IPinfoClient') + IPinfo.stubs(:create).returns(@mock_ipinfo_client) + + @mock_details = OpenStruct.new( + ip: '1.2.3.4', + city: 'New York', + country: 'US', + hostname: 'example.com', + org: 'Example Org' + ) + end + + # Custom IP Selector + class CustomIPSelector + def initialize(request) + @request = request + end + + def get_ip + '9.10.11.12' + end + end + + def test_should_use_default_ip_selector_when_no_custom_selector_is_provided + @mock_ipinfo_client.expects(:details).with('1.2.3.4').returns(@mock_details) + + @middleware = IPinfoMiddleware.new(@app, token: 'test_token') + request = Rack::MockRequest.new(@middleware) + + # Simulate a request with REMOTE_ADDR + env = { 'REMOTE_ADDR' => '1.2.3.4' } + response = request.get('/', env) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_equal '1.2.3.4', @app.last_env['ipinfo'].ip + assert_equal 'New York', @app.last_env['ipinfo'].city + end + + def test_should_use_custom_ip_selector_when_provided + @mock_ipinfo_client.expects(:details).with('9.10.11.12') + .returns(@mock_details.dup.tap { |d| d.ip = '9.10.11.12' }) + + @middleware = IPinfoMiddleware.new(@app, + token: 'test_token', + ip_selector: CustomIPSelector) + request = Rack::MockRequest.new(@middleware) + + response = request.get('/', {}) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_equal '9.10.11.12', @app.last_env['ipinfo'].ip + end + + def test_middleware_skips_processing_if_filter_returns_true + always_filter = ->(_request) { true } + + @middleware = IPinfoMiddleware.new(@app, + token: 'test_token', + filter: always_filter) + request = Rack::MockRequest.new(@middleware) + + @mock_ipinfo_client.expects(:details).never + + response = request.get('/', { 'REMOTE_ADDR' => '8.8.8.8' }) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_nil @app.last_env['ipinfo'], + 'ipinfo should be nil when filtered' + end + + def test_middleware_processes_if_filter_returns_false + never_filter = ->(_request) { false } + @mock_ipinfo_client.expects(:details).with('1.2.3.4').returns(@mock_details) + + @middleware = IPinfoMiddleware.new(@app, + token: 'test_token', + filter: never_filter) + request = Rack::MockRequest.new(@middleware) + + response = request.get('/', { 'REMOTE_ADDR' => '1.2.3.4' }) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_equal '1.2.3.4', @app.last_env['ipinfo'].ip + end + + def test_middleware_filters_bots_by_default + @mock_ipinfo_client.expects(:details).never # Should not call if bot + + @middleware = IPinfoMiddleware.new(@app, token: 'test_token') + request = Rack::MockRequest.new(@middleware) + + # Test with common bot user agents + bot_env = { 'HTTP_USER_AGENT' => 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' } + response = request.get('/', bot_env) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_nil @app.last_env['ipinfo'], + 'ipinfo should be nil for bot user agent' + + spider_env = { 'HTTP_USER_AGENT' => 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)' } + response = request.get('/', spider_env) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_nil @app.last_env['ipinfo'], + 'ipinfo should be nil for spider user agent' + end + + def test_middleware_does_not_filter_non_bots_by_default + @mock_ipinfo_client.expects(:details).with('1.2.3.4').returns(@mock_details) + + @middleware = IPinfoMiddleware.new(@app, token: 'test_token') + request = Rack::MockRequest.new(@middleware) + + # Test with a regular user agent + user_env = { 'REMOTE_ADDR' => '1.2.3.4', 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } + response = request.get('/', user_env) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_equal '1.2.3.4', @app.last_env['ipinfo'].ip + end + + def test_middleware_handles_missing_user_agent + @mock_ipinfo_client.expects(:details).with('1.2.3.4').returns(@mock_details) + + @middleware = IPinfoMiddleware.new(@app, token: 'test_token') + request = Rack::MockRequest.new(@middleware) + + # Test with no user agent provided + no_ua_env = { 'REMOTE_ADDR' => '1.2.3.4' } + response = request.get('/', no_ua_env) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_equal '1.2.3.4', @app.last_env['ipinfo'].ip + end + + def test_middleware_handles_ipinfo_api_errors + @mock_ipinfo_client.expects(:details).raises(StandardError, + 'API rate limit exceeded') + + @middleware = IPinfoMiddleware.new(@app, token: 'test_token') + request = Rack::MockRequest.new(@middleware) + + assert_raises StandardError do + request.get('/', { 'REMOTE_ADDR' => '1.2.3.4' }) + end + end +end From d164ddd02ec83c2fb6cfa543a5111efe25390a1e Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 1 Aug 2025 16:46:11 +0200 Subject: [PATCH 6/8] Add support for Lite API --- README.md | 15 ++- lib/ipinfo-rails.rb | 45 +++++-- test/ipinfo_lite_middleware_test.rb | 181 ++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 test/ipinfo_lite_middleware_test.rb diff --git a/README.md b/README.md index 96db530..81f7a71 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ You'll need an IPinfo API access token, which you can get by signing up for a fr The free plan is limited to 50,000 requests per month, and doesn't include some of the data fields such as IP type and company data. To enable all the data fields and additional request volumes see [https://ipinfo.io/pricing](https://ipinfo.io/pricing) -⚠️ Note: This library does not currently support our newest free API https://ipinfo.io/lite. If you’d like to use IPinfo Lite, you can call the [endpoint directly](https://ipinfo.io/developers/lite-api) using your preferred HTTP client. Developers are also welcome to contribute support for Lite by submitting a pull request. +The library also supports the Lite API, see the [Lite API section](#lite-api) for more info. ### Installation @@ -246,6 +246,19 @@ config.middleware.use(IPinfoMiddleware, { This simple lambda function will filter out requests coming from your local computer. +## Lite API + +The library gives the possibility to use the [Lite API](https://ipinfo.io/developers/lite-api) too, authentication with your token is still required. + +The returned details are slightly different from the Core API, but it has the same configurations options. + +You can use it like so: + +```ruby +require 'ipinfo-rails' +config.middleware.use(IPinfoLiteMiddleware, {token: ""}) +``` + ## Other Libraries There are official IPinfo client libraries available for many languages including PHP, Go, Java, Ruby, and many popular frameworks such as Django, Rails, and Laravel. There are also many third-party libraries and integrations available for our API. diff --git a/lib/ipinfo-rails.rb b/lib/ipinfo-rails.rb index 9c73c59..ecb5cbd 100644 --- a/lib/ipinfo-rails.rb +++ b/lib/ipinfo-rails.rb @@ -2,8 +2,18 @@ require 'rack' require 'ipinfo' +require 'ipinfo_lite' require 'ipinfo-rails/ip_selector/default_ip_selector' +def is_bot(request) + if request.user_agent + user_agent = request.user_agent.downcase + user_agent.include?('bot') || user_agent.include?('spider') + else + false + end +end + class IPinfoMiddleware def initialize(app, options = {}) @app = app @@ -32,15 +42,34 @@ def call(env) @app.call(env) end +end - private +class IPinfoLiteMiddleware + def initialize(app, options = {}) + @app = app + @token = options.fetch(:token, nil) + @ipinfo = IPinfoLite.create(@token, options) + @filter = options.fetch(:filter, nil) + @ip_selector = options.fetch(:ip_selector, DefaultIPSelector) + end - def is_bot(request) - if request.user_agent - user_agent = request.user_agent.downcase - user_agent.include?('bot') || user_agent.include?('spider') - else - false - end + def call(env) + env['called'] = 'yes' + request = Rack::Request.new(env) + ip_selector = @ip_selector.new(request) + filtered = if @filter.nil? + is_bot(request) + else + @filter.call(request) + end + + if filtered + env['ipinfo'] = nil + else + ip = ip_selector.get_ip + env['ipinfo'] = @ipinfo.details(ip) end + + @app.call(env) + end end diff --git a/test/ipinfo_lite_middleware_test.rb b/test/ipinfo_lite_middleware_test.rb new file mode 100644 index 0000000..2a146b0 --- /dev/null +++ b/test/ipinfo_lite_middleware_test.rb @@ -0,0 +1,181 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require 'minitest/mock' +require 'mocha/minitest' +require 'rack/mock' +require 'ostruct' +require 'ipinfo_lite' +require 'ipinfo/errors' +require_relative '../lib/ipinfo-rails' + + +# Simple Rack app +class TestApp + attr_reader :last_env + + def call(env) + @last_env = env + [200, { 'Content-Type' => 'text/plain' }, ['Hello from TestApp!']] + end +end + +class IPinfoLiteMiddlewareTest < Minitest::Test + def setup + @app = TestApp.new + @middleware = nil + @mock_ipinfo_client = mock('IPinfoClient') + IPinfoLite.stubs(:create).returns(@mock_ipinfo_client) + + @mock_details = OpenStruct.new( + ip: '1.2.3.4', + city: 'New York', + country: 'US', + hostname: 'example.com', + org: 'Example Org' + ) + end + + # Custom IP Selector + class CustomIPSelector + def initialize(request) + @request = request + end + + def get_ip + '9.10.11.12' + end + end + + def test_should_use_default_ip_selector_when_no_custom_selector_is_provided + @mock_ipinfo_client.expects(:details).with('1.2.3.4').returns(@mock_details) + + @middleware = IPinfoLiteMiddleware.new(@app, token: 'test_token') + request = Rack::MockRequest.new(@middleware) + + # Simulate a request with REMOTE_ADDR + env = { 'REMOTE_ADDR' => '1.2.3.4' } + response = request.get('/', env) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_equal '1.2.3.4', @app.last_env['ipinfo'].ip + assert_equal 'New York', @app.last_env['ipinfo'].city + end + + def test_should_use_custom_ip_selector_when_provided + @mock_ipinfo_client.expects(:details).with('9.10.11.12') + .returns(@mock_details.dup.tap { |d| d.ip = '9.10.11.12' }) + + @middleware = IPinfoLiteMiddleware.new(@app, + token: 'test_token', + ip_selector: CustomIPSelector) + request = Rack::MockRequest.new(@middleware) + + response = request.get('/', {}) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_equal '9.10.11.12', @app.last_env['ipinfo'].ip + end + + def test_middleware_skips_processing_if_filter_returns_true + always_filter = ->(_request) { true } + + @middleware = IPinfoLiteMiddleware.new(@app, + token: 'test_token', + filter: always_filter) + request = Rack::MockRequest.new(@middleware) + + @mock_ipinfo_client.expects(:details).never + + response = request.get('/', { 'REMOTE_ADDR' => '8.8.8.8' }) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_nil @app.last_env['ipinfo'], + 'ipinfo should be nil when filtered' + end + + def test_middleware_processes_if_filter_returns_false + never_filter = ->(_request) { false } + @mock_ipinfo_client.expects(:details).with('1.2.3.4').returns(@mock_details) + + @middleware = IPinfoLiteMiddleware.new(@app, + token: 'test_token', + filter: never_filter) + request = Rack::MockRequest.new(@middleware) + + response = request.get('/', { 'REMOTE_ADDR' => '1.2.3.4' }) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_equal '1.2.3.4', @app.last_env['ipinfo'].ip + end + + def test_middleware_filters_bots_by_default + @mock_ipinfo_client.expects(:details).never # Should not call if bot + + @middleware = IPinfoLiteMiddleware.new(@app, token: 'test_token') + request = Rack::MockRequest.new(@middleware) + + # Test with common bot user agents + bot_env = { 'HTTP_USER_AGENT' => 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' } + response = request.get('/', bot_env) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_nil @app.last_env['ipinfo'], + 'ipinfo should be nil for bot user agent' + + spider_env = { 'HTTP_USER_AGENT' => 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)' } + response = request.get('/', spider_env) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_nil @app.last_env['ipinfo'], + 'ipinfo should be nil for spider user agent' + end + + def test_middleware_does_not_filter_non_bots_by_default + @mock_ipinfo_client.expects(:details).with('1.2.3.4').returns(@mock_details) + + @middleware = IPinfoLiteMiddleware.new(@app, token: 'test_token') + request = Rack::MockRequest.new(@middleware) + + # Test with a regular user agent + user_env = { 'REMOTE_ADDR' => '1.2.3.4', 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } + response = request.get('/', user_env) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_equal '1.2.3.4', @app.last_env['ipinfo'].ip + end + + def test_middleware_handles_missing_user_agent + @mock_ipinfo_client.expects(:details).with('1.2.3.4').returns(@mock_details) + + @middleware = IPinfoLiteMiddleware.new(@app, token: 'test_token') + request = Rack::MockRequest.new(@middleware) + + # Test with no user agent provided + no_ua_env = { 'REMOTE_ADDR' => '1.2.3.4' } + response = request.get('/', no_ua_env) + + assert_equal 200, response.status + assert_equal 'yes', @app.last_env['called'] + assert_equal '1.2.3.4', @app.last_env['ipinfo'].ip + end + + def test_middleware_handles_ipinfo_api_errors + @mock_ipinfo_client.expects(:details).raises(StandardError, + 'API rate limit exceeded') + + @middleware = IPinfoLiteMiddleware.new(@app, token: 'test_token') + request = Rack::MockRequest.new(@middleware) + + assert_raises StandardError do + request.get('/', { 'REMOTE_ADDR' => '1.2.3.4' }) + end + end +end From e7ca0a3fc289e25900aa53104b07e0d4cdd26f14 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 1 Aug 2025 16:52:25 +0200 Subject: [PATCH 7/8] Install rake in CI before running it --- .github/workflows/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4691cac..609c5d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,5 +27,8 @@ jobs: - name: Install dependencies run: bundle install + - name: Install rake + run: gem install rake + - name: Run tests - run: bundle exec rake + run: rake From 1626fc761a5c2579c0a5a5391928211867b57ac2 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 1 Aug 2025 16:53:29 +0200 Subject: [PATCH 8/8] Install rake properly --- .github/workflows/test.yml | 5 +---- Gemfile | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 609c5d9..4691cac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,8 +27,5 @@ jobs: - name: Install dependencies run: bundle install - - name: Install rake - run: gem install rake - - name: Run tests - run: rake + run: bundle exec rake diff --git a/Gemfile b/Gemfile index c6e73e0..89f216c 100644 --- a/Gemfile +++ b/Gemfile @@ -8,5 +8,6 @@ group :development do gem 'bundler' gem 'minitest' gem 'minitest-reporters' + gem 'rake' gem 'rubocop' end