mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 19:23:37 +08:00
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:
parent
4b7ab97a01
commit
e1e392f15b
|
@ -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);
|
||||||
|
|
|
@ -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"}}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
begin
|
@loc_mmdb = mmdb_load(File.join(path, 'GeoLite2-City.mmdb'))
|
||||||
@mmdb_filename = File.join(path, 'GeoLite2-City.mmdb')
|
@asn_mmdb = mmdb_load(File.join(path, 'GeoLite2-ASN.mmdb'))
|
||||||
@mmdb = MaxMindDB.new(@mmdb_filename, MaxMindDB::LOW_MEMORY_FILE_READER)
|
|
||||||
@cache = LruRedux::ThreadSafeCache.new(1000)
|
@cache = LruRedux::ThreadSafeCache.new(1000)
|
||||||
|
end
|
||||||
|
|
||||||
|
def mmdb_load(filepath)
|
||||||
|
begin
|
||||||
|
MaxMindDB.new(filepath, MaxMindDB::LOW_MEMORY_FILE_READER)
|
||||||
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 = {}
|
||||||
|
|
||||||
|
if @loc_mmdb
|
||||||
begin
|
begin
|
||||||
result = @mmdb.lookup(ip)
|
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
|
rescue
|
||||||
Rails.logger.error("IP #{ip} could not be looked up in MaxMindDB.")
|
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
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,10 @@ 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")
|
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)
|
tar_gz_archive = Net::HTTP.get(uri)
|
||||||
|
|
||||||
extractor = Gem::Package::TarReader.new(Zlib::GzipReader.new(StringIO.new(tar_gz_archive)))
|
extractor = Gem::Package::TarReader.new(Zlib::GzipReader.new(StringIO.new(tar_gz_archive)))
|
||||||
|
@ -13,8 +15,8 @@ task "maxminddb:get" do
|
||||||
extractor.each do |entry|
|
extractor.each do |entry|
|
||||||
next unless entry.full_name.ends_with?(".mmdb")
|
next unless entry.full_name.ends_with?(".mmdb")
|
||||||
|
|
||||||
filename = File.join(Rails.root, 'vendor', 'data', 'GeoLite2-City.mmdb')
|
filename = File.join(Rails.root, 'vendor', 'data', "#{name}.mmdb")
|
||||||
puts "Writing #{filename}"
|
puts "Writing #{filename}..."
|
||||||
File.open(filename, "wb") do |f|
|
File.open(filename, "wb") do |f|
|
||||||
f.write(entry.read)
|
f.write(entry.read)
|
||||||
end
|
end
|
||||||
|
@ -22,3 +24,7 @@ task "maxminddb:get" do
|
||||||
|
|
||||||
extractor.close
|
extractor.close
|
||||||
end
|
end
|
||||||
|
|
||||||
|
download_mmdb('GeoLite2-City')
|
||||||
|
download_mmdb('GeoLite2-ASN')
|
||||||
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user