Skip to content

Commit 20e6ae1

Browse files
authored
Merge pull request #17 from ipinfo/silvano/eng-654-add-resproxy-support-in-ipinforails-library
Add Residential Proxy API support
2 parents 8713c71 + c872793 commit 20e6ae1

File tree

4 files changed

+226
-2
lines changed

4 files changed

+226
-2
lines changed

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ gemspec
66

77
group :development do
88
gem 'bundler'
9-
gem 'minitest'
9+
gem 'minitest', '< 6'
1010
gem 'minitest-reporters'
1111
gem 'rake'
1212
gem 'rubocop'

ipinfo-rails.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
2121
s.homepage = 'https://ipinfo.io'
2222
s.license = 'Apache-2.0'
2323

24-
s.add_dependency 'IPinfo', '~> 2.4'
24+
s.add_dependency 'IPinfo', '~> 2.5'
2525
s.add_dependency 'rack', '~> 2.0'
2626

2727
s.add_development_dependency 'mocha', '~> 2.7'

lib/ipinfo-rails.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,33 @@ def call(env)
135135
@app.call(env)
136136
end
137137
end
138+
139+
class IPinfoResproxyMiddleware
140+
def initialize(app, options = {})
141+
@app = app
142+
@token = options.fetch(:token, nil)
143+
@ipinfo = IPinfo.create(@token, options)
144+
@filter = options.fetch(:filter, nil)
145+
@ip_selector = options.fetch(:ip_selector, DefaultIPSelector)
146+
end
147+
148+
def call(env)
149+
env['called'] = 'yes'
150+
request = Rack::Request.new(env)
151+
ip_selector = @ip_selector.new(request)
152+
filtered = if @filter.nil?
153+
is_bot(request)
154+
else
155+
@filter.call(request)
156+
end
157+
158+
if filtered
159+
env['ipinfo_resproxy'] = nil
160+
else
161+
ip = ip_selector.get_ip
162+
env['ipinfo_resproxy'] = @ipinfo.resproxy(ip)
163+
end
164+
165+
@app.call(env)
166+
end
167+
end
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# frozen_string_literal: true
2+
3+
require 'minitest/autorun'
4+
require 'mocha/minitest'
5+
require 'rack/mock'
6+
require 'ostruct'
7+
require 'ipinfo'
8+
require 'ipinfo/errors'
9+
require_relative '../lib/ipinfo-rails'
10+
11+
12+
# Simple Rack app
13+
class TestApp
14+
attr_reader :last_env
15+
16+
def call(env)
17+
@last_env = env
18+
[200, { 'Content-Type' => 'text/plain' }, ['Hello from TestApp!']]
19+
end
20+
end
21+
22+
class IPinfoResproxyMiddlewareTest < Minitest::Test
23+
def setup
24+
@app = TestApp.new
25+
@middleware = nil
26+
@mock_ipinfo_client = mock('IPinfoClient')
27+
IPinfo.stubs(:create).returns(@mock_ipinfo_client)
28+
29+
@mock_resproxy = OpenStruct.new(
30+
ip: '175.107.211.204',
31+
last_seen: '2026-01-15',
32+
percent_days_seen: 100,
33+
service: 'test_service'
34+
)
35+
end
36+
37+
# Custom IP Selector
38+
class CustomIPSelector
39+
def initialize(request)
40+
@request = request
41+
end
42+
43+
def get_ip
44+
'9.10.11.12'
45+
end
46+
end
47+
48+
def test_should_use_default_ip_selector_when_no_custom_selector_is_provided
49+
@mock_ipinfo_client.expects(:resproxy).with('175.107.211.204').returns(@mock_resproxy)
50+
51+
@middleware = IPinfoResproxyMiddleware.new(@app, token: 'test_token')
52+
request = Rack::MockRequest.new(@middleware)
53+
54+
# Simulate a request with REMOTE_ADDR
55+
env = { 'REMOTE_ADDR' => '175.107.211.204' }
56+
response = request.get('/', env)
57+
58+
assert_equal 200, response.status
59+
assert_equal 'yes', @app.last_env['called']
60+
assert_equal '175.107.211.204', @app.last_env['ipinfo_resproxy'].ip
61+
assert_equal 'test_service', @app.last_env['ipinfo_resproxy'].service
62+
end
63+
64+
def test_should_use_custom_ip_selector_when_provided
65+
@mock_ipinfo_client.expects(:resproxy).with('9.10.11.12')
66+
.returns(@mock_resproxy.dup.tap { |d| d.ip = '9.10.11.12' })
67+
68+
@middleware = IPinfoResproxyMiddleware.new(@app,
69+
token: 'test_token',
70+
ip_selector: CustomIPSelector)
71+
request = Rack::MockRequest.new(@middleware)
72+
73+
response = request.get('/', {})
74+
75+
assert_equal 200, response.status
76+
assert_equal 'yes', @app.last_env['called']
77+
assert_equal '9.10.11.12', @app.last_env['ipinfo_resproxy'].ip
78+
end
79+
80+
def test_middleware_skips_processing_if_filter_returns_true
81+
always_filter = ->(_request) { true }
82+
83+
@middleware = IPinfoResproxyMiddleware.new(@app,
84+
token: 'test_token',
85+
filter: always_filter)
86+
request = Rack::MockRequest.new(@middleware)
87+
88+
@mock_ipinfo_client.expects(:resproxy).never
89+
90+
response = request.get('/', { 'REMOTE_ADDR' => '8.8.8.8' })
91+
92+
assert_equal 200, response.status
93+
assert_equal 'yes', @app.last_env['called']
94+
assert_nil @app.last_env['ipinfo_resproxy'],
95+
'ipinfo_resproxy should be nil when filtered'
96+
end
97+
98+
def test_middleware_processes_if_filter_returns_false
99+
never_filter = ->(_request) { false }
100+
@mock_ipinfo_client.expects(:resproxy).with('175.107.211.204').returns(@mock_resproxy)
101+
102+
@middleware = IPinfoResproxyMiddleware.new(@app,
103+
token: 'test_token',
104+
filter: never_filter)
105+
request = Rack::MockRequest.new(@middleware)
106+
107+
response = request.get('/', { 'REMOTE_ADDR' => '175.107.211.204' })
108+
109+
assert_equal 200, response.status
110+
assert_equal 'yes', @app.last_env['called']
111+
assert_equal '175.107.211.204', @app.last_env['ipinfo_resproxy'].ip
112+
end
113+
114+
def test_middleware_filters_bots_by_default
115+
@mock_ipinfo_client.expects(:resproxy).never # Should not call if bot
116+
117+
@middleware = IPinfoResproxyMiddleware.new(@app, token: 'test_token')
118+
request = Rack::MockRequest.new(@middleware)
119+
120+
# Test with common bot user agents
121+
bot_env = { 'HTTP_USER_AGENT' => 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' }
122+
response = request.get('/', bot_env)
123+
124+
assert_equal 200, response.status
125+
assert_equal 'yes', @app.last_env['called']
126+
assert_nil @app.last_env['ipinfo_resproxy'],
127+
'ipinfo_resproxy should be nil for bot user agent'
128+
129+
spider_env = { 'HTTP_USER_AGENT' => 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)' }
130+
response = request.get('/', spider_env)
131+
132+
assert_equal 200, response.status
133+
assert_equal 'yes', @app.last_env['called']
134+
assert_nil @app.last_env['ipinfo_resproxy'],
135+
'ipinfo_resproxy should be nil for spider user agent'
136+
end
137+
138+
def test_middleware_does_not_filter_non_bots_by_default
139+
@mock_ipinfo_client.expects(:resproxy).with('175.107.211.204').returns(@mock_resproxy)
140+
141+
@middleware = IPinfoResproxyMiddleware.new(@app, token: 'test_token')
142+
request = Rack::MockRequest.new(@middleware)
143+
144+
# Test with a regular user agent
145+
user_env = { 'REMOTE_ADDR' => '175.107.211.204', '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' }
146+
response = request.get('/', user_env)
147+
148+
assert_equal 200, response.status
149+
assert_equal 'yes', @app.last_env['called']
150+
assert_equal '175.107.211.204', @app.last_env['ipinfo_resproxy'].ip
151+
end
152+
153+
def test_middleware_handles_missing_user_agent
154+
@mock_ipinfo_client.expects(:resproxy).with('175.107.211.204').returns(@mock_resproxy)
155+
156+
@middleware = IPinfoResproxyMiddleware.new(@app, token: 'test_token')
157+
request = Rack::MockRequest.new(@middleware)
158+
159+
# Test with no user agent provided
160+
no_ua_env = { 'REMOTE_ADDR' => '175.107.211.204' }
161+
response = request.get('/', no_ua_env)
162+
163+
assert_equal 200, response.status
164+
assert_equal 'yes', @app.last_env['called']
165+
assert_equal '175.107.211.204', @app.last_env['ipinfo_resproxy'].ip
166+
end
167+
168+
def test_middleware_handles_ipinfo_api_errors
169+
@mock_ipinfo_client.expects(:resproxy).raises(StandardError,
170+
'API rate limit exceeded')
171+
172+
@middleware = IPinfoResproxyMiddleware.new(@app, token: 'test_token')
173+
request = Rack::MockRequest.new(@middleware)
174+
175+
assert_raises StandardError do
176+
request.get('/', { 'REMOTE_ADDR' => '175.107.211.204' })
177+
end
178+
end
179+
180+
def test_middleware_passes_through_empty_response
181+
# Empty response simulates IP not in resproxy database
182+
empty_response = OpenStruct.new({})
183+
@mock_ipinfo_client.expects(:resproxy).with('175.107.211.204').returns(empty_response)
184+
185+
@middleware = IPinfoResproxyMiddleware.new(@app, token: 'test_token')
186+
request = Rack::MockRequest.new(@middleware)
187+
188+
response = request.get('/', { 'REMOTE_ADDR' => '175.107.211.204' })
189+
190+
assert_equal 200, response.status
191+
assert_equal 'yes', @app.last_env['called']
192+
assert_equal empty_response, @app.last_env['ipinfo_resproxy']
193+
end
194+
end

0 commit comments

Comments
 (0)