diff --git a/app/assets/javascripts/admin/components/ip-lookup.js.es6 b/app/assets/javascripts/admin/components/ip-lookup.js.es6 index 4717b365028..357ba10d5f5 100644 --- a/app/assets/javascripts/admin/components/ip-lookup.js.es6 +++ b/app/assets/javascripts/admin/components/ip-lookup.js.es6 @@ -5,16 +5,6 @@ import copyText from "discourse/lib/copy-text"; export default Ember.Component.extend({ classNames: ["ip-lookup"], - city: function() { - return [ - this.get("location.city"), - this.get("location.region"), - this.get("location.country") - ] - .filter(Boolean) - .join(", "); - }.property("location.{city,region,country}"), - otherAccountsToDelete: function() { // can only delete up to 50 accounts at a time var total = Math.min(50, this.get("totalOthersWithSameIP") || 0); @@ -72,24 +62,19 @@ export default Ember.Component.extend({ } text += I18n.t("ip_lookup.location"); - if (location.loc) { - text += `: ${location.loc} ${this.get("city")}\n`; + if (location.location) { + text += `: ${location.location}\n`; } else { text += `: ${I18n.t("ip_lookup.location_not_found")}\n`; } - if (location.org) { + if (location.organization) { text += I18n.t("ip_lookup.organisation"); - text += `: ${location.org}\n`; - } - - if (location.phone) { - text += I18n.t("ip_lookup.phone"); - text += `: ${location.phone}\n`; + text += `: ${location.organization}\n`; } } const copyRange = $('

'); - copyRange.html(text.trim().replace("\n", "
")); + copyRange.html(text.trim().replace(/\n/g, "
")); $(document.body).append(copyRange); if (copyText(text, copyRange[0])) { this.set("copied", true); diff --git a/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs b/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs index fec7b7d0229..573db188560 100644 --- a/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs +++ b/app/assets/javascripts/discourse/templates/components/ip-lookup.hbs @@ -22,22 +22,16 @@
{{i18n 'ip_lookup.location'}}
- {{#if location.loc}} - {{location.loc}}
- {{city}} + {{#if location.location}} + {{location.location}} {{else}} {{i18n 'ip_lookup.location_not_found'}} {{/if}}
- {{#if location.org}} + {{#if location.organization}}
{{i18n 'ip_lookup.organisation'}}
-
{{location.org}}
- {{/if}} - - {{#if location.phone}} -
{{i18n 'ip_lookup.phone'}}
-
{{location.phone}}
+
{{location.organization}}
{{/if}} {{else}} {{loading-spinner size="small"}} diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index e73015e67cd..776568e4c51 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -435,18 +435,8 @@ class Admin::UsersController < Admin::AdminController def ip_info params.require(:ip) - ip = params[:ip] - # should we cache results in redis? - begin - location = Excon.get( - "https://ipinfo.io/#{ip}/json", - read_timeout: 10, connect_timeout: 10 - )&.body - rescue Excon::Error - end - - render json: location + render json: DiscourseIpInfo.get(params[:ip]) end def sync_sso diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 74a99dbf438..275fab90f88 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -597,7 +597,7 @@ en: topics_entered: "topics entered" post_count: "# posts" confirm_delete_other_accounts: "Are you sure you want to delete these accounts?" - powered_by: "powered by ipinfo.io" + powered_by: "using MaxMindDB" copied: "copied" user_fields: diff --git a/lib/discourse_ip_info.rb b/lib/discourse_ip_info.rb index 11703cff79d..e01c4457ec4 100644 --- a/lib/discourse_ip_info.rb +++ b/lib/discourse_ip_info.rb @@ -1,4 +1,5 @@ require_dependency 'maxminddb' +require_dependency 'resolv' class DiscourseIpInfo include Singleton @@ -8,10 +9,14 @@ class DiscourseIpInfo end def open_db(path) + @loc_mmdb = mmdb_load(File.join(path, 'GeoLite2-City.mmdb')) + @asn_mmdb = mmdb_load(File.join(path, 'GeoLite2-ASN.mmdb')) + @cache = LruRedux::ThreadSafeCache.new(1000) + end + + def mmdb_load(filepath) begin - @mmdb_filename = File.join(path, 'GeoLite2-City.mmdb') - @mmdb = MaxMindDB.new(@mmdb_filename, MaxMindDB::LOW_MEMORY_FILE_READER) - @cache = LruRedux::ThreadSafeCache.new(1000) + MaxMindDB.new(filepath, MaxMindDB::LOW_MEMORY_FILE_READER) rescue Errno::ENOENT => e Rails.logger.warn("MaxMindDB could not be found: #{e}") rescue @@ -20,30 +25,51 @@ class DiscourseIpInfo end def lookup(ip, locale = :en) - return {} unless @mmdb + ret = {} - begin - result = @mmdb.lookup(ip) - rescue - Rails.logger.error("IP #{ip} could not be looked up in MaxMindDB.") + if @loc_mmdb + begin + result = @loc_mmdb.lookup(ip) + if result&.found? + ret[:country] = result.country.name(locale) || result.country.name + ret[:country_code] = result.country.iso_code + ret[:region] = result.subdivisions.most_specific.name(locale) || result.subdivisions.most_specific.name + ret[:city] = result.city.name(locale) || result.city.name + ret[:latitude] = result.location.latitude + ret[:longitude] = result.location.longitude + ret[:location] = [ret[:city], ret[:region], ret[:country]].reject(&:blank?).join(", ") + end + rescue + Rails.logger.error("IP #{ip} could not be looked up in MaxMind GeoLite2-City database.") + end end - return {} if !result || !result.found? + if @asn_mmdb + begin + result = @asn_mmdb.lookup(ip) + if result&.found? + result = result.to_hash + ret[:asn] = result["autonomous_system_number"] + ret[:organization] = result["autonomous_system_organization"] + end + rescue + Rails.logger.error("IP #{ip} could not be looked up in MaxMind GeoLite2-ASN database.") + end + end - locale = locale.to_s.sub('_', '-') + begin + result = Resolv::DNS.new.getname(ip) + ret[:hostname] = result&.to_s + rescue Resolv::ResolvError + end - { - country: result.country.name(locale) || result.country.name, - country_code: result.country.iso_code, - region: result.subdivisions.most_specific.name(locale) || result.subdivisions.most_specific.name, - city: result.city.name(locale) || result.city.name, - } + ret end def get(ip, locale = :en) - return {} unless @mmdb - ip = ip.to_s + locale = locale.to_s.sub('_', '-') + @cache["#{ip}-#{locale}"] ||= lookup(ip, locale) end diff --git a/lib/tasks/maxminddb.rake b/lib/tasks/maxminddb.rake index 79b31d2899a..9ce9ea995be 100644 --- a/lib/tasks/maxminddb.rake +++ b/lib/tasks/maxminddb.rake @@ -3,22 +3,28 @@ require 'zlib' desc "downloads MaxMind's GeoLite2-City database" task "maxminddb:get" do - puts "Downloading maxmind db" - uri = URI("http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz") - tar_gz_archive = Net::HTTP.get(uri) - extractor = Gem::Package::TarReader.new(Zlib::GzipReader.new(StringIO.new(tar_gz_archive))) - extractor.rewind + def download_mmdb(name) + puts "Downloading MaxMindDb #{name}" + uri = URI("http://geolite.maxmind.com/download/geoip/database/#{name}.tar.gz") + tar_gz_archive = Net::HTTP.get(uri) - extractor.each do |entry| - next unless entry.full_name.ends_with?(".mmdb") + extractor = Gem::Package::TarReader.new(Zlib::GzipReader.new(StringIO.new(tar_gz_archive))) + extractor.rewind - filename = File.join(Rails.root, 'vendor', 'data', 'GeoLite2-City.mmdb') - puts "Writing #{filename}" - File.open(filename, "wb") do |f| - f.write(entry.read) + extractor.each do |entry| + next unless entry.full_name.ends_with?(".mmdb") + + filename = File.join(Rails.root, 'vendor', 'data', "#{name}.mmdb") + puts "Writing #{filename}..." + File.open(filename, "wb") do |f| + f.write(entry.read) + end end + + extractor.close end - extractor.close + download_mmdb('GeoLite2-City') + download_mmdb('GeoLite2-ASN') end diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb index 886eeae9fbf..224131c87ea 100644 --- a/spec/requests/admin/users_controller_spec.rb +++ b/spec/requests/admin/users_controller_spec.rb @@ -1,4 +1,5 @@ require 'rails_helper' +require 'discourse_ip_info' RSpec.describe Admin::UsersController do let(:admin) { Fabricate(:admin) } @@ -710,19 +711,24 @@ RSpec.describe Admin::UsersController do end describe '#ip_info' do - it "uses ipinfo.io webservice to retrieve the info" do - ip = "192.168.1.1" - ip_data = { - city: "Jeddah", - country: "SA", - ip: ip - } - url = "https://ipinfo.io/#{ip}/json" + it "retrieves IP info" do + ip = "81.2.69.142" + + DiscourseIpInfo.open_db(File.join(Rails.root, 'spec', 'fixtures', 'mmdb')) + Resolv::DNS.any_instance.stubs(:getname).with(ip).returns("ip-81-2-69-142.example.com") - stub_request(:get, url).to_return(status: 200, body: ip_data.to_json) get "/admin/users/ip-info.json", params: { ip: ip } expect(response.status).to eq(200) - expect(JSON.parse(response.body).symbolize_keys).to eq(ip_data) + expect(JSON.parse(response.body).symbolize_keys).to eq( + city: "London", + country: "United Kingdom", + country_code: "GB", + hostname: "ip-81-2-69-142.example.com", + location: "London, England, United Kingdom", + region: "England", + latitude: 51.5142, + longitude: -0.0931, + ) end end