DEV: Use DiscourseIpInfo for all IP queries. (#6482)

* DEV: Use DiscourseIpInfo for all IP queries.

* UX: Use latitude and longitude for more precision.
This commit is contained in:
Bianca Nenciu 2018-10-31 00:08:57 +02:00 committed by Régis Hanol
parent 4b7ab97a01
commit e1e392f15b
7 changed files with 89 additions and 82 deletions

View File

@ -5,16 +5,6 @@ import copyText from "discourse/lib/copy-text";
export default Ember.Component.extend({ export default Ember.Component.extend({
classNames: ["ip-lookup"], 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() { otherAccountsToDelete: function() {
// can only delete up to 50 accounts at a time // can only delete up to 50 accounts at a time
var total = Math.min(50, this.get("totalOthersWithSameIP") || 0); var total = Math.min(50, this.get("totalOthersWithSameIP") || 0);
@ -72,24 +62,19 @@ export default Ember.Component.extend({
} }
text += I18n.t("ip_lookup.location"); text += I18n.t("ip_lookup.location");
if (location.loc) { if (location.location) {
text += `: ${location.loc} ${this.get("city")}\n`; text += `: ${location.location}\n`;
} else { } else {
text += `: ${I18n.t("ip_lookup.location_not_found")}\n`; text += `: ${I18n.t("ip_lookup.location_not_found")}\n`;
} }
if (location.org) { if (location.organization) {
text += I18n.t("ip_lookup.organisation"); text += I18n.t("ip_lookup.organisation");
text += `: ${location.org}\n`; text += `: ${location.organization}\n`;
}
if (location.phone) {
text += I18n.t("ip_lookup.phone");
text += `: ${location.phone}\n`;
} }
} }
const copyRange = $('<p id="copy-range"></p>'); const copyRange = $('<p id="copy-range"></p>');
copyRange.html(text.trim().replace("\n", "<br>")); copyRange.html(text.trim().replace(/\n/g, "<br>"));
$(document.body).append(copyRange); $(document.body).append(copyRange);
if (copyText(text, copyRange[0])) { if (copyText(text, copyRange[0])) {
this.set("copied", true); this.set("copied", true);

View File

@ -22,22 +22,16 @@
<dt>{{i18n 'ip_lookup.location'}}</dt> <dt>{{i18n 'ip_lookup.location'}}</dt>
<dd> <dd>
{{#if location.loc}} {{#if location.location}}
<a href="https://maps.google.com/maps?q={{unbound location.loc}}" target="_blank">{{location.loc}}</a><br> <a href="https://maps.google.com/maps?q={{unbound location.latitude}},{{unbound location.longitude}}" target="_blank">{{location.location}}</a>
{{city}}
{{else}} {{else}}
{{i18n 'ip_lookup.location_not_found'}} {{i18n 'ip_lookup.location_not_found'}}
{{/if}} {{/if}}
</dd> </dd>
{{#if location.org}} {{#if location.organization}}
<dt>{{i18n 'ip_lookup.organisation'}}</dt> <dt>{{i18n 'ip_lookup.organisation'}}</dt>
<dd>{{location.org}}</dd> <dd>{{location.organization}}</dd>
{{/if}}
{{#if location.phone}}
<dt>{{i18n 'ip_lookup.phone'}}</dt>
<dd>{{location.phone}}</dd>
{{/if}} {{/if}}
{{else}} {{else}}
{{loading-spinner size="small"}} {{loading-spinner size="small"}}

View File

@ -435,18 +435,8 @@ class Admin::UsersController < Admin::AdminController
def ip_info def ip_info
params.require(:ip) params.require(:ip)
ip = params[:ip]
# should we cache results in redis? render json: DiscourseIpInfo.get(params[:ip])
begin
location = Excon.get(
"https://ipinfo.io/#{ip}/json",
read_timeout: 10, connect_timeout: 10
)&.body
rescue Excon::Error
end
render json: location
end end
def sync_sso def sync_sso

View File

@ -597,7 +597,7 @@ en:
topics_entered: "topics entered" topics_entered: "topics entered"
post_count: "# posts" post_count: "# posts"
confirm_delete_other_accounts: "Are you sure you want to delete these accounts?" confirm_delete_other_accounts: "Are you sure you want to delete these accounts?"
powered_by: "powered by <a href='https://ipinfo.io'>ipinfo.io</a>" powered_by: "using <a href='https://maxmind.com'>MaxMindDB</a>"
copied: "copied" copied: "copied"
user_fields: user_fields:

View File

@ -1,4 +1,5 @@
require_dependency 'maxminddb' require_dependency 'maxminddb'
require_dependency 'resolv'
class DiscourseIpInfo class DiscourseIpInfo
include Singleton include Singleton
@ -8,10 +9,14 @@ class DiscourseIpInfo
end end
def open_db(path) 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 begin
@mmdb_filename = File.join(path, 'GeoLite2-City.mmdb') MaxMindDB.new(filepath, MaxMindDB::LOW_MEMORY_FILE_READER)
@mmdb = MaxMindDB.new(@mmdb_filename, MaxMindDB::LOW_MEMORY_FILE_READER)
@cache = LruRedux::ThreadSafeCache.new(1000)
rescue Errno::ENOENT => e rescue Errno::ENOENT => e
Rails.logger.warn("MaxMindDB could not be found: #{e}") Rails.logger.warn("MaxMindDB could not be found: #{e}")
rescue rescue
@ -20,30 +25,51 @@ class DiscourseIpInfo
end end
def lookup(ip, locale = :en) def lookup(ip, locale = :en)
return {} unless @mmdb ret = {}
begin if @loc_mmdb
result = @mmdb.lookup(ip) begin
rescue result = @loc_mmdb.lookup(ip)
Rails.logger.error("IP #{ip} could not be looked up in MaxMindDB.") 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 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
{ ret
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,
}
end end
def get(ip, locale = :en) def get(ip, locale = :en)
return {} unless @mmdb
ip = ip.to_s ip = ip.to_s
locale = locale.to_s.sub('_', '-')
@cache["#{ip}-#{locale}"] ||= lookup(ip, locale) @cache["#{ip}-#{locale}"] ||= lookup(ip, locale)
end end

View File

@ -3,22 +3,28 @@ require 'zlib'
desc "downloads MaxMind's GeoLite2-City database" desc "downloads MaxMind's GeoLite2-City database"
task "maxminddb:get" do 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))) def download_mmdb(name)
extractor.rewind 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| extractor = Gem::Package::TarReader.new(Zlib::GzipReader.new(StringIO.new(tar_gz_archive)))
next unless entry.full_name.ends_with?(".mmdb") extractor.rewind
filename = File.join(Rails.root, 'vendor', 'data', 'GeoLite2-City.mmdb') extractor.each do |entry|
puts "Writing #{filename}" next unless entry.full_name.ends_with?(".mmdb")
File.open(filename, "wb") do |f|
f.write(entry.read) filename = File.join(Rails.root, 'vendor', 'data', "#{name}.mmdb")
puts "Writing #{filename}..."
File.open(filename, "wb") do |f|
f.write(entry.read)
end
end end
extractor.close
end end
extractor.close download_mmdb('GeoLite2-City')
download_mmdb('GeoLite2-ASN')
end end

View File

@ -1,4 +1,5 @@
require 'rails_helper' require 'rails_helper'
require 'discourse_ip_info'
RSpec.describe Admin::UsersController do RSpec.describe Admin::UsersController do
let(:admin) { Fabricate(:admin) } let(:admin) { Fabricate(:admin) }
@ -710,19 +711,24 @@ RSpec.describe Admin::UsersController do
end end
describe '#ip_info' do describe '#ip_info' do
it "uses ipinfo.io webservice to retrieve the info" do it "retrieves IP info" do
ip = "192.168.1.1" ip = "81.2.69.142"
ip_data = {
city: "Jeddah", DiscourseIpInfo.open_db(File.join(Rails.root, 'spec', 'fixtures', 'mmdb'))
country: "SA", Resolv::DNS.any_instance.stubs(:getname).with(ip).returns("ip-81-2-69-142.example.com")
ip: ip
}
url = "https://ipinfo.io/#{ip}/json"
stub_request(:get, url).to_return(status: 200, body: ip_data.to_json)
get "/admin/users/ip-info.json", params: { ip: ip } get "/admin/users/ip-info.json", params: { ip: ip }
expect(response.status).to eq(200) 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
end end