Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,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

Expand Down Expand Up @@ -217,6 +217,23 @@ details.all = {
}
```

### 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.

```ruby
require 'ipinfo_lite'

access_token = '123456789abc'
handler = IPinfoLite::create(access_token)

details = handler.details('8.8.8.8')
details.country_code # US
details.country # United States
```

#### Caching

In-memory caching of `details` data is provided by default via the
Expand Down
39 changes: 39 additions & 0 deletions lib/ipinfo/adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,42 @@ def default_headers
headers
end
end

class IPinfo::AdapterLite
HOST = 'https://api.ipinfo.io/lite/'

attr_reader :conn

def initialize(token = nil, adapter = :net_http)
@token = token
@conn = connection(adapter)
end

def get(uri)
@conn.get(HOST + uri) do |req|
default_headers.each_pair do |key, value|
req.headers[key] = value
end
req.params['token'] = CGI.escape(token) if token
end
end

private

attr_reader :token

def connection(adapter)
Faraday.new() do |conn|
conn.adapter(adapter)
end
end

def default_headers
headers = {
'User-Agent' => "IPinfoClient/Ruby/#{IPinfo::VERSION}",
'Accept' => 'application/json'
}
headers['Authorization'] = "Bearer #{CGI.escape(token)}" if token
headers
end
end
3 changes: 3 additions & 0 deletions lib/ipinfo/mod.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@

module IPinfo
end

module IPinfoLite
end
116 changes: 116 additions & 0 deletions lib/ipinfo_lite.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# frozen_string_literal: true

require 'ipinfo/adapter'
require 'ipinfo/cache/default_cache'
require 'ipinfo/errors'
require 'ipinfo/response'
require_relative 'ipinfo/ipAddressMatcher'
require_relative 'ipinfo/countriesData'
require 'ipaddr'
require 'cgi'

module IPinfoLite
include CountriesData
DEFAULT_CACHE_MAXSIZE = 4096
DEFAULT_CACHE_TTL = 60 * 60 * 24
RATE_LIMIT_MESSAGE = 'To increase your limits, please review our ' \
'paid plans at https://ipinfo.io/pricing'
# Base URL to get country flag image link.
# "PK" -> "https://cdn.ipinfo.io/static/images/countries-flags/PK.svg"
COUNTRY_FLAGS_URL = 'https://cdn.ipinfo.io/static/images/countries-flags/'

class << self
def create(access_token = nil, settings = {})
IPinfo::IPinfoLite.new(access_token, settings)
end
end
end

class IPinfo::IPinfoLite
include IPinfoLite
attr_accessor :access_token, :countries, :httpc

def initialize(access_token = nil, settings = {})
@access_token = access_token
@httpc = IPinfo::AdapterLite.new(access_token, httpc || :net_http)

maxsize = settings.fetch('maxsize', DEFAULT_CACHE_MAXSIZE)
ttl = settings.fetch('ttl', DEFAULT_CACHE_TTL)
@cache = settings.fetch('cache', IPinfo::DefaultCache.new(ttl, maxsize))
@countries = settings.fetch('countries', DEFAULT_COUNTRY_LIST)
@eu_countries = settings.fetch('eu_countries', DEFAULT_EU_COUNTRIES_LIST)
@countries_flags = settings.fetch('countries_flags', DEFAULT_COUNTRIES_FLAG_LIST)
@countries_currencies = settings.fetch('countries_currencies', DEFAULT_COUNTRIES_CURRENCIES_LIST)
@continents = settings.fetch('continents', DEFAULT_CONTINENT_LIST)
end

def details(ip_address = nil)
details_base(ip_address)
end

def request_details(ip_address = nil)
if ip_address && ip_address != 'me' && isBogon(ip_address)
details[:ip] = ip_address
details[:bogon] = true
details[:ip_address] = IPAddr.new(ip_address)

return details
end

res = @cache.get(cache_key(ip_address))
return res unless res.nil?

ip_address ||= 'me'
response = @httpc.get(escape_path(ip_address))

if response.status.eql?(429)
raise RateLimitError,
RATE_LIMIT_MESSAGE
end

details = JSON.parse(response.body, symbolize_names: true)
@cache.set(cache_key(ip_address), details)
details
end

def details_base(ip_address)
details = request_details(ip_address)
if details.key? :country_code
details[:country_name] =
@countries.fetch(details.fetch(:country_code), nil)
details[:is_eu] =
@eu_countries.include?(details.fetch(:country_code))
details[:country_flag] =
@countries_flags.fetch(details.fetch(:country_code), nil)
details[:country_currency] =
@countries_currencies.fetch(details.fetch(:country_code), nil)
details[:continent] =
@continents.fetch(details.fetch(:country_code), nil)
details[:country_flag_url] = "#{COUNTRY_FLAGS_URL}#{details.fetch(:country_code)}.svg"
end

if details.key? :ip
details[:ip_address] =
IPAddr.new(details.fetch(:ip))
end

IPinfo::Response.new(details)
end

def isBogon(ip)
if ip.nil?
return false
end

matcher_object = IPinfo::IpAddressMatcher.new(ip)
matcher_object.matches
end

def escape_path(ip)
ip ? "/#{CGI.escape(ip)}" : '/'
end

def cache_key(ip)
"1:#{ip}"
end
end
75 changes: 75 additions & 0 deletions test/ipinfo_lite_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

require_relative 'test_helper'

class IPinfoLiteTest < Minitest::Test
TEST_IPV4 = '8.8.8.8'
TEST_IPV6 = '2001:240:2a54:3900::'

def assert_ip6(resp)
assert_equal(resp.ip, TEST_IPV6)
assert_equal(resp.ip_address, IPAddr.new(TEST_IPV6))
assert_equal(resp.country, 'Japan')
assert_equal(resp.country_code, 'JP')
assert_equal(resp.country_name, 'Japan')
assert_equal(resp.is_eu, false)
assert_equal(resp.country_flag['emoji'], '🇯🇵')
assert_equal(resp.country_flag['unicode'], 'U+1F1EF U+1F1F5')
assert_equal(resp.country_flag_url, 'https://cdn.ipinfo.io/static/images/countries-flags/JP.svg')
assert_equal(resp.country_currency['code'], 'JPY')
assert_equal(resp.country_currency['symbol'], '¥')
assert_equal(resp.continent['code'], 'AS')
assert_equal(resp.continent['name'], 'Asia')
assert_equal(resp.asn, 'AS2497')
end

def assert_ip4(resp)
assert_equal(resp.ip, TEST_IPV4)
assert_equal(resp.ip_address, IPAddr.new(TEST_IPV4))
assert_equal(resp.country, 'United States')
assert_equal(resp.country_code, 'US')
assert_equal(resp.country_name, 'United States')
assert_equal(resp.is_eu, false)
assert_equal(resp.country_flag['emoji'], '🇺🇸')
assert_equal(resp.country_flag['unicode'], 'U+1F1FA U+1F1F8')
assert_equal(resp.country_flag_url, 'https://cdn.ipinfo.io/static/images/countries-flags/US.svg')
assert_equal(resp.country_currency['code'], 'USD')
assert_equal(resp.country_currency['symbol'], '$')
assert_equal(resp.continent['code'], 'NA')
assert_equal(resp.continent['name'], 'North America')
assert_equal(resp.asn,'AS15169')
end

def test_that_it_has_a_version_number
refute_nil ::IPinfo::VERSION
end

def test_set_adapter_v4
ipinfo = IPinfoLite.create(
ENV.fetch('IPINFO_TOKEN', nil),
{ http_client: :excon }
)

assert(ipinfo.httpc = :excon)
end

def test_lookup_ip6
ipinfo = IPinfoLite.create(ENV.fetch('IPINFO_TOKEN', nil))

# multiple checks for cache
(0...5).each do |_|
resp = ipinfo.details(TEST_IPV6)
assert_ip6(resp)
end
end

def test_lookup_ip4
ipinfo = IPinfoLite.create(ENV.fetch('IPINFO_TOKEN', nil))

# multiple checks for cache
(0...5).each do |_|
resp = ipinfo.details(TEST_IPV4)
assert_ip4(resp)
end
end
end
4 changes: 2 additions & 2 deletions test/ipinfo_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def assert_ip6(resp)
assert_equal(
resp.company,
{
"name": 'Internet Initiative Japan Inc.',
"name": 'IIJ Internet',
"domain": 'iij.ad.jp',
"type": 'isp'
}
Expand Down Expand Up @@ -74,7 +74,7 @@ def assert_ip4(resp)
assert_equal(resp.ip, TEST_IPV4)
assert_equal(resp.ip_address, IPAddr.new(TEST_IPV4))
assert_equal(resp.hostname, 'dns.google')
assert_equal(resp.anycast, true)
assert_equal(resp.is_anycast, true)
assert_equal(resp.city, 'Mountain View')
assert_equal(resp.region, 'California')
assert_equal(resp.country, 'US')
Expand Down
1 change: 1 addition & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
require 'ipinfo'
require 'ipinfo_lite'

require 'minitest/autorun'
require 'minitest/reporters'
Expand Down