Skip to content

Commit 11c7272

Browse files
authored
Merge pull request #56 from ipinfo/silvano/eng-498-add-core-bundle-support-in-ipinforuby-library
Add support for Core bundle
2 parents 8320708 + 098986d commit 11c7272

File tree

4 files changed

+290
-0
lines changed

4 files changed

+290
-0
lines changed

lib/ipinfo/adapter.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,42 @@ def default_headers
9191
headers
9292
end
9393
end
94+
95+
class IPinfo::AdapterCore
96+
HOST = 'https://api.ipinfo.io/lookup'
97+
98+
attr_reader :conn
99+
100+
def initialize(token = nil, adapter = :net_http)
101+
@token = token
102+
@conn = connection(adapter)
103+
end
104+
105+
def get(uri)
106+
@conn.get(HOST + uri) do |req|
107+
default_headers.each_pair do |key, value|
108+
req.headers[key] = value
109+
end
110+
req.params['token'] = CGI.escape(token) if token
111+
end
112+
end
113+
114+
private
115+
116+
attr_reader :token
117+
118+
def connection(adapter)
119+
Faraday.new() do |conn|
120+
conn.adapter(adapter)
121+
end
122+
end
123+
124+
def default_headers
125+
headers = {
126+
'User-Agent' => "IPinfoClient/Ruby/#{IPinfo::VERSION}",
127+
'Accept' => 'application/json'
128+
}
129+
headers['Authorization'] = "Bearer #{CGI.escape(token)}" if token
130+
headers
131+
end
132+
end

lib/ipinfo_core.rb

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# frozen_string_literal: true
2+
3+
require 'ipinfo/adapter'
4+
require 'ipinfo/cache/default_cache'
5+
require 'ipinfo/errors'
6+
require 'ipinfo/response'
7+
require_relative 'ipinfo/ipAddressMatcher'
8+
require_relative 'ipinfo/countriesData'
9+
require 'ipaddr'
10+
require 'cgi'
11+
12+
module IPinfoCore
13+
include CountriesData
14+
DEFAULT_CACHE_MAXSIZE = 4096
15+
DEFAULT_CACHE_TTL = 60 * 60 * 24
16+
RATE_LIMIT_MESSAGE = 'To increase your limits, please review our ' \
17+
'paid plans at https://ipinfo.io/pricing'
18+
# Base URL to get country flag image link.
19+
# "PK" -> "https://cdn.ipinfo.io/static/images/countries-flags/PK.svg"
20+
COUNTRY_FLAGS_URL = 'https://cdn.ipinfo.io/static/images/countries-flags/'
21+
22+
class << self
23+
def create(access_token = nil, settings = {})
24+
IPinfo::IPinfoCore.new(access_token, settings)
25+
end
26+
end
27+
end
28+
29+
class IPinfo::IPinfoCore
30+
include IPinfoCore
31+
attr_accessor :access_token, :countries, :httpc
32+
33+
def initialize(access_token = nil, settings = {})
34+
@access_token = access_token
35+
@httpc = IPinfo::AdapterCore.new(access_token, httpc || :net_http)
36+
37+
maxsize = settings.fetch('maxsize', DEFAULT_CACHE_MAXSIZE)
38+
ttl = settings.fetch('ttl', DEFAULT_CACHE_TTL)
39+
@cache = settings.fetch('cache', IPinfo::DefaultCache.new(ttl, maxsize))
40+
@countries = settings.fetch('countries', DEFAULT_COUNTRY_LIST)
41+
@eu_countries = settings.fetch('eu_countries', DEFAULT_EU_COUNTRIES_LIST)
42+
@countries_flags = settings.fetch('countries_flags', DEFAULT_COUNTRIES_FLAG_LIST)
43+
@countries_currencies = settings.fetch('countries_currencies', DEFAULT_COUNTRIES_CURRENCIES_LIST)
44+
@continents = settings.fetch('continents', DEFAULT_CONTINENT_LIST)
45+
end
46+
47+
def details(ip_address = nil)
48+
details_base(ip_address)
49+
end
50+
51+
def request_details(ip_address = nil)
52+
if ip_address && isBogon(ip_address)
53+
details = {}
54+
details[:ip] = ip_address
55+
details[:bogon] = true
56+
details[:ip_address] = IPAddr.new(ip_address)
57+
return details
58+
end
59+
60+
res = @cache.get(cache_key(ip_address))
61+
return res unless res.nil?
62+
63+
response = @httpc.get(escape_path(ip_address))
64+
65+
if response.status.eql?(429)
66+
raise RateLimitError,
67+
RATE_LIMIT_MESSAGE
68+
end
69+
70+
details = JSON.parse(response.body, symbolize_names: true)
71+
@cache.set(cache_key(ip_address), details)
72+
details
73+
end
74+
75+
def details_base(ip_address)
76+
details = request_details(ip_address)
77+
78+
# Core response has nested geo object
79+
if details.key?(:geo) && details[:geo].is_a?(Hash) && details[:geo].key?(:country_code)
80+
country_code = details[:geo][:country_code]
81+
details[:geo][:country_name] = @countries.fetch(country_code, nil)
82+
details[:geo][:is_eu] = @eu_countries.include?(country_code)
83+
details[:geo][:country_flag] = @countries_flags.fetch(country_code, nil)
84+
details[:geo][:country_currency] = @countries_currencies.fetch(country_code, nil)
85+
details[:geo][:continent] = @continents.fetch(country_code, nil)
86+
details[:geo][:country_flag_url] = "#{COUNTRY_FLAGS_URL}#{country_code}.svg"
87+
end
88+
89+
# Handle top-level country_code if present (for certain edge cases)
90+
if details.key?(:country_code)
91+
country_code = details[:country_code]
92+
details[:country_name] = @countries.fetch(country_code, nil)
93+
details[:is_eu] = @eu_countries.include?(country_code)
94+
details[:country_flag] = @countries_flags.fetch(country_code, nil)
95+
details[:country_currency] = @countries_currencies.fetch(country_code, nil)
96+
details[:continent] = @continents.fetch(country_code, nil)
97+
details[:country_flag_url] = "#{COUNTRY_FLAGS_URL}#{country_code}.svg"
98+
end
99+
100+
if details.key? :ip
101+
details[:ip_address] =
102+
IPAddr.new(details.fetch(:ip))
103+
end
104+
105+
IPinfo::Response.new(details)
106+
end
107+
108+
def isBogon(ip)
109+
if ip.nil?
110+
return false
111+
end
112+
113+
matcher_object = IPinfo::IpAddressMatcher.new(ip)
114+
matcher_object.matches
115+
end
116+
117+
def escape_path(ip)
118+
ip ? "/#{CGI.escape(ip)}" : '/'
119+
end
120+
121+
def cache_key(ip)
122+
"1:#{ip}"
123+
end
124+
end

test/ipinfo_core_test.rb

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'test_helper'
4+
5+
class IPinfoCoreTest < Minitest::Test
6+
TEST_IPV4 = '8.8.8.8'
7+
TEST_IPV6 = '2001:4860:4860::8888'
8+
9+
def test_set_adapter
10+
ipinfo = IPinfoCore.create(
11+
ENV.fetch('IPINFO_TOKEN', nil),
12+
{ 'http_client' => :excon }
13+
)
14+
15+
assert(ipinfo.httpc = :excon)
16+
end
17+
18+
def test_lookup_ip4
19+
ipinfo = IPinfoCore.create(ENV.fetch('IPINFO_TOKEN', nil))
20+
21+
# multiple checks for cache
22+
(0...5).each do |_|
23+
resp = ipinfo.details(TEST_IPV4)
24+
25+
# Basic fields
26+
assert_equal(resp.ip, TEST_IPV4)
27+
assert_equal(resp.ip_address, IPAddr.new(TEST_IPV4))
28+
assert_equal(resp.hostname, 'dns.google')
29+
30+
# Geo object assertions
31+
assert(resp.geo.is_a?(Hash))
32+
refute_nil(resp.geo[:city])
33+
refute_nil(resp.geo[:region])
34+
refute_nil(resp.geo[:region_code])
35+
assert_equal(resp.geo[:country_code], 'US')
36+
assert_equal(resp.geo[:country], 'United States')
37+
assert_equal(resp.geo[:country_name], 'United States')
38+
assert_equal(resp.geo[:is_eu], false)
39+
refute_nil(resp.geo[:continent])
40+
refute_nil(resp.geo[:continent_code])
41+
refute_nil(resp.geo[:latitude])
42+
refute_nil(resp.geo[:longitude])
43+
refute_nil(resp.geo[:timezone])
44+
refute_nil(resp.geo[:postal_code])
45+
assert_equal(resp.geo[:country_flag]['emoji'], '🇺🇸')
46+
assert_equal(resp.geo[:country_flag]['unicode'], 'U+1F1FA U+1F1F8')
47+
assert_equal(resp.geo[:country_flag_url], 'https://cdn.ipinfo.io/static/images/countries-flags/US.svg')
48+
assert_equal(resp.geo[:country_currency]['code'], 'USD')
49+
assert_equal(resp.geo[:country_currency]['symbol'], '$')
50+
assert_equal(resp.geo[:continent]['code'], 'NA')
51+
assert_equal(resp.geo[:continent]['name'], 'North America')
52+
53+
# AS object assertions
54+
assert(resp.as.is_a?(Hash))
55+
assert_equal(resp.as[:asn], 'AS15169')
56+
assert(resp.as[:name].is_a?(String))
57+
assert(resp.as[:domain].is_a?(String))
58+
assert(resp.as[:type].is_a?(String))
59+
60+
# Network flags
61+
assert_equal(resp.is_anonymous, false)
62+
assert_equal(resp.is_anycast, true)
63+
assert_equal(resp.is_hosting, true)
64+
assert_equal(resp.is_mobile, false)
65+
assert_equal(resp.is_satellite, false)
66+
end
67+
end
68+
69+
def test_lookup_ip6
70+
ipinfo = IPinfoCore.create(ENV.fetch('IPINFO_TOKEN', nil))
71+
72+
# multiple checks for cache
73+
(0...5).each do |_|
74+
resp = ipinfo.details(TEST_IPV6)
75+
76+
# Basic fields
77+
assert_equal(resp.ip, TEST_IPV6)
78+
assert_equal(resp.ip_address, IPAddr.new(TEST_IPV6))
79+
80+
# Geo object assertions
81+
assert(resp.geo.is_a?(Hash))
82+
refute_nil(resp.geo[:city])
83+
refute_nil(resp.geo[:region])
84+
refute_nil(resp.geo[:region_code])
85+
assert_equal(resp.geo[:country_code], 'US')
86+
assert_equal(resp.geo[:country], 'United States')
87+
assert_equal(resp.geo[:country_name], 'United States')
88+
assert_equal(resp.geo[:is_eu], false)
89+
refute_nil(resp.geo[:continent])
90+
refute_nil(resp.geo[:continent_code])
91+
refute_nil(resp.geo[:latitude])
92+
refute_nil(resp.geo[:longitude])
93+
refute_nil(resp.geo[:timezone])
94+
refute_nil(resp.geo[:postal_code])
95+
assert_equal(resp.geo[:country_flag]['emoji'], '🇺🇸')
96+
assert_equal(resp.geo[:country_flag]['unicode'], 'U+1F1FA U+1F1F8')
97+
assert_equal(resp.geo[:country_flag_url], 'https://cdn.ipinfo.io/static/images/countries-flags/US.svg')
98+
assert_equal(resp.geo[:country_currency]['code'], 'USD')
99+
assert_equal(resp.geo[:country_currency]['symbol'], '$')
100+
assert_equal(resp.geo[:continent]['code'], 'NA')
101+
assert_equal(resp.geo[:continent]['name'], 'North America')
102+
103+
# AS object assertions
104+
assert(resp.as.is_a?(Hash))
105+
assert_equal(resp.as[:asn], 'AS15169')
106+
assert(resp.as[:name].is_a?(String))
107+
assert(resp.as[:domain].is_a?(String))
108+
assert(resp.as[:type].is_a?(String))
109+
110+
# Network flags
111+
assert_equal(resp.is_anonymous, false)
112+
assert_equal(resp.is_anycast, true)
113+
assert_equal(resp.is_hosting, true)
114+
assert_equal(resp.is_mobile, false)
115+
assert_equal(resp.is_satellite, false)
116+
end
117+
end
118+
119+
def test_bogon_ip
120+
ipinfo = IPinfoCore.create(ENV.fetch('IPINFO_TOKEN', nil))
121+
122+
resp = ipinfo.details('192.168.1.1')
123+
assert_equal(resp.bogon, true)
124+
assert_equal(resp.ip, '192.168.1.1')
125+
end
126+
end

test/test_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
44
require 'ipinfo'
55
require 'ipinfo_lite'
6+
require 'ipinfo_core'
67

78
require 'minitest/autorun'
89
require 'minitest/reporters'

0 commit comments

Comments
 (0)