From 69b077b469a779a674c6eca8d215c845ae86928e Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Wed, 25 Jun 2025 16:24:08 +0200 Subject: [PATCH 1/3] Fix failing tests --- test/ipinfo_test.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/ipinfo_test.rb b/test/ipinfo_test.rb index cdf3e04..900d35c 100644 --- a/test/ipinfo_test.rb +++ b/test/ipinfo_test.rb @@ -9,15 +9,15 @@ class IPinfoTest < Minitest::Test def assert_ip6(resp) assert_equal(resp.ip, TEST_IPV6) assert_equal(resp.ip_address, IPAddr.new(TEST_IPV6)) - assert_equal(resp.city, 'Hiroshima') - assert_equal(resp.region, 'Hiroshima') + assert_equal(resp.city, 'Osaka') + assert_equal(resp.region, 'Osaka') assert_equal(resp.country, 'JP') assert_equal(resp.country_name, 'Japan') assert_equal(resp.is_eu, false) - assert_equal(resp.loc, '34.4000,132.4500') - assert_equal(resp.latitude, '34.4000') - assert_equal(resp.longitude, '132.4500') - assert_equal(resp.postal, '730-0011') + assert_equal(resp.loc, '34.6938,135.5011') + assert_equal(resp.latitude, '34.6938') + assert_equal(resp.longitude, '135.5011') + assert_equal(resp.postal, '543-0062') assert_equal(resp.timezone, 'Asia/Tokyo') assert_equal(resp.country_flag_url, 'https://cdn.ipinfo.io/static/images/countries-flags/JP.svg') assert_equal( @@ -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' } @@ -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') From 9bb28b8117094c3e5af42d5248e5f634a3d1a3c0 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Fri, 27 Jun 2025 09:35:03 +0200 Subject: [PATCH 2/3] Add support for Lite API --- README.md | 19 ++++++- lib/ipinfo/adapter.rb | 39 +++++++++++++ lib/ipinfo/mod.rb | 3 + lib/ipinfo_lite.rb | 116 +++++++++++++++++++++++++++++++++++++++ test/ipinfo_lite_test.rb | 75 +++++++++++++++++++++++++ test/test_helper.rb | 1 + 6 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 lib/ipinfo_lite.rb create mode 100644 test/ipinfo_lite_test.rb diff --git a/README.md b/README.md index 7117bc6..9a9ca39 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/lib/ipinfo/adapter.rb b/lib/ipinfo/adapter.rb index a984586..23380ee 100644 --- a/lib/ipinfo/adapter.rb +++ b/lib/ipinfo/adapter.rb @@ -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 diff --git a/lib/ipinfo/mod.rb b/lib/ipinfo/mod.rb index fe2150a..c3a2da5 100644 --- a/lib/ipinfo/mod.rb +++ b/lib/ipinfo/mod.rb @@ -2,3 +2,6 @@ module IPinfo end + +module IPinfoLite +end diff --git a/lib/ipinfo_lite.rb b/lib/ipinfo_lite.rb new file mode 100644 index 0000000..f8206f0 --- /dev/null +++ b/lib/ipinfo_lite.rb @@ -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 diff --git a/test/ipinfo_lite_test.rb b/test/ipinfo_lite_test.rb new file mode 100644 index 0000000..0811ef2 --- /dev/null +++ b/test/ipinfo_lite_test.rb @@ -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 diff --git a/test/test_helper.rb b/test/test_helper.rb index 11735d6..f7ba267 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,6 +2,7 @@ $LOAD_PATH.unshift File.expand_path('../lib', __dir__) require 'ipinfo' +require 'ipinfo_lite' require 'minitest/autorun' require 'minitest/reporters' From 3e4f99b5c905d0b7848c8ece7395e15b20571016 Mon Sep 17 00:00:00 2001 From: Silvano Cerza Date: Thu, 3 Jul 2025 16:17:58 +0200 Subject: [PATCH 3/3] Fix tests --- test/ipinfo_test.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/ipinfo_test.rb b/test/ipinfo_test.rb index 900d35c..a7478aa 100644 --- a/test/ipinfo_test.rb +++ b/test/ipinfo_test.rb @@ -9,15 +9,15 @@ class IPinfoTest < Minitest::Test def assert_ip6(resp) assert_equal(resp.ip, TEST_IPV6) assert_equal(resp.ip_address, IPAddr.new(TEST_IPV6)) - assert_equal(resp.city, 'Osaka') - assert_equal(resp.region, 'Osaka') + assert_equal(resp.city, 'Hiroshima') + assert_equal(resp.region, 'Hiroshima') assert_equal(resp.country, 'JP') assert_equal(resp.country_name, 'Japan') assert_equal(resp.is_eu, false) - assert_equal(resp.loc, '34.6938,135.5011') - assert_equal(resp.latitude, '34.6938') - assert_equal(resp.longitude, '135.5011') - assert_equal(resp.postal, '543-0062') + assert_equal(resp.loc, '34.4000,132.4500') + assert_equal(resp.latitude, '34.4000') + assert_equal(resp.longitude, '132.4500') + assert_equal(resp.postal, '730-0011') assert_equal(resp.timezone, 'Asia/Tokyo') assert_equal(resp.country_flag_url, 'https://cdn.ipinfo.io/static/images/countries-flags/JP.svg') assert_equal(