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/ipinfo_test.rb b/test/ipinfo_test.rb index cdf3e04..a7478aa 100644 --- a/test/ipinfo_test.rb +++ b/test/ipinfo_test.rb @@ -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') 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'