2018-11-18 22:50:21 -06:00
# frozen_string_literal: true
2018-11-01 17:16:45 -05:00
require " maxminddb "
require " resolv "
2018-10-09 09:21:41 -05:00
class DiscourseIpInfo
include Singleton
def initialize
2019-05-20 19:48:18 -05:00
open_db ( DiscourseIpInfo . path )
2018-10-25 05:54:01 -05:00
end
def open_db ( path )
2023-11-23 16:38:46 -06:00
@loc_mmdb = mmdb_load ( File . join ( path , " GeoLite2-City.mmdb " ) )
@asn_mmdb = mmdb_load ( File . join ( path , " GeoLite2-ASN.mmdb " ) )
2018-10-30 20:38:57 -05:00
@cache = LruRedux :: ThreadSafeCache . new ( 2000 )
2018-10-30 17:08:57 -05:00
end
2019-05-20 19:48:18 -05:00
def self . path
@path || = File . join ( Rails . root , " vendor " , " data " )
end
2019-04-10 04:37:29 -05:00
def self . mmdb_path ( name )
2019-05-20 19:48:18 -05:00
File . join ( path , " #{ name } .mmdb " )
2019-04-10 04:37:29 -05:00
end
def self . mmdb_download ( name )
2024-05-09 02:11:56 -05:00
extra_headers = { }
2020-01-02 23:31:28 -06:00
url =
2024-04-02 01:53:53 -05:00
if GlobalSetting . maxmind_mirror_url . present?
2024-04-07 21:27:40 -05:00
File . join ( GlobalSetting . maxmind_mirror_url , " #{ name } .tar.gz " ) . to_s
2024-04-02 01:53:53 -05:00
else
2024-05-09 02:11:56 -05:00
license_key = GlobalSetting . maxmind_license_key
if license_key . blank?
STDERR . puts " MaxMind IP database download requires an account ID and a license key "
STDERR . puts " Please set DISCOURSE_MAXMIND_ACCOUNT_ID and DISCOURSE_MAXMIND_LICENSE_KEY. See https://meta.discourse.org/t/configure-maxmind-for-reverse-ip-lookups/173941 for more details. "
2024-04-02 01:53:53 -05:00
return
end
2024-05-09 02:11:56 -05:00
account_id = GlobalSetting . maxmind_account_id
if account_id . present?
extra_headers [
" Authorization "
] = " Basic #{ Base64 . strict_encode64 ( " #{ account_id } : #{ license_key } " ) } "
" https://download.maxmind.com/geoip/databases/ #{ name } /download?suffix=tar.gz "
else
# This URL is not documented by MaxMind, but it works but we don't know when it will stop working. Therefore,
# we are deprecating this in 3.3 and will remove it in 3.4. An admin dashboard warning has been added to inform
# site admins about this deprecation. See `ProblemCheck::MaxmindDbConfiguration` for more information.
" https://download.maxmind.com/app/geoip_download?license_key= #{ license_key } &edition_id= #{ name } &suffix=tar.gz "
end
2024-04-02 01:53:53 -05:00
end
2020-01-02 23:31:28 -06:00
2019-05-27 01:51:24 -05:00
gz_file =
FileHelper . download (
2020-01-02 23:31:28 -06:00
url ,
2019-05-27 01:51:24 -05:00
max_file_size : 100 . megabytes ,
2019-05-27 19:28:57 -05:00
tmp_file_name : " #{ name } .gz " ,
validate_uri : false ,
2024-03-25 17:39:09 -05:00
follow_redirect : true ,
2024-05-09 02:11:56 -05:00
extra_headers : ,
2019-05-27 01:51:24 -05:00
)
2020-01-05 05:08:13 -06:00
filename = File . basename ( gz_file . path )
dir = " #{ Dir . tmpdir } / #{ SecureRandom . hex } "
Discourse :: Utils . execute_command ( " mkdir " , " -p " , dir )
Discourse :: Utils . execute_command ( " cp " , gz_file . path , " #{ dir } / #{ filename } " )
Discourse :: Utils . execute_command ( " tar " , " -xzvf " , " #{ dir } / #{ filename } " , chdir : dir )
2019-05-27 01:51:24 -05:00
2020-01-05 05:08:13 -06:00
Dir [ " #{ dir } /**/*.mmdb " ] . each { | f | FileUtils . mv ( f , mmdb_path ( name ) ) }
2019-05-24 08:13:19 -05:00
ensure
2020-01-05 05:08:13 -06:00
FileUtils . rm_r ( dir , force : true ) if dir
2019-05-24 15:11:10 -05:00
gz_file & . close!
2019-04-10 04:37:29 -05:00
end
2018-10-30 17:08:57 -05:00
def mmdb_load ( filepath )
2018-10-09 09:21:41 -05:00
begin
2018-10-30 17:08:57 -05:00
MaxMindDB . new ( filepath , MaxMindDB :: LOW_MEMORY_FILE_READER )
2018-10-09 09:21:41 -05:00
rescue Errno :: ENOENT = > e
2018-10-30 20:57:18 -05:00
Rails . logger . warn ( " MaxMindDB ( #{ filepath } ) could not be found: #{ e } " )
nil
rescue = > e
2019-05-27 20:45:12 -05:00
Discourse . warn_exception ( e , message : " MaxMindDB ( #{ filepath } ) could not be loaded. " )
2018-10-30 20:57:18 -05:00
nil
2018-10-09 09:21:41 -05:00
end
end
2018-10-30 20:38:57 -05:00
def lookup ( ip , locale : :en , resolve_hostname : false )
2018-10-30 17:08:57 -05:00
ret = { }
2018-11-18 22:50:21 -06:00
return ret if ip . blank?
2018-10-09 09:21:41 -05:00
2018-10-30 17:08:57 -05:00
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
2018-12-14 06:47:59 -06:00
ret [ :location ] = ret . values_at ( :city , :region , :country ) . reject ( & :blank? ) . uniq . join ( " , " )
2022-03-02 15:51:42 -06:00
2022-03-02 19:24:58 -06:00
# used by plugins or API to locate users more accurately
2022-03-02 15:51:42 -06:00
ret [ :geoname_ids ] = [
result . continent . geoname_id ,
result . country . geoname_id ,
result . city . geoname_id ,
* result . subdivisions . map ( & :geoname_id ) ,
]
ret [ :geoname_ids ] . compact!
2018-10-30 17:08:57 -05:00
end
2018-10-30 20:57:18 -05:00
rescue = > e
2018-10-30 21:37:54 -05:00
Discourse . warn_exception (
e ,
message : " IP #{ ip } could not be looked up in MaxMind GeoLite2-City database. " ,
)
2018-10-30 17:08:57 -05:00
end
2018-10-09 09:21:41 -05:00
end
2018-10-30 17:08:57 -05:00
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
2018-10-30 20:57:18 -05:00
rescue = > e
2018-10-30 21:37:54 -05:00
Discourse . warn_exception (
e ,
message : " IP #{ ip } could not be looked up in MaxMind GeoLite2-ASN database. " ,
)
2018-10-30 17:08:57 -05:00
end
end
2018-10-09 09:21:41 -05:00
2018-10-30 20:38:57 -05:00
# this can block for quite a while
# only use it explicitly when needed
if resolve_hostname
begin
result = Resolv :: DNS . new . getname ( ip )
ret [ :hostname ] = result & . to_s
rescue Resolv :: ResolvError
end
2018-10-30 17:08:57 -05:00
end
2018-10-25 05:54:01 -05:00
2018-10-30 17:08:57 -05:00
ret
2018-10-09 09:21:41 -05:00
end
2018-10-30 20:38:57 -05:00
def get ( ip , locale : :en , resolve_hostname : false )
2018-10-25 04:45:31 -05:00
ip = ip . to_s
2018-10-30 17:08:57 -05:00
locale = locale . to_s . sub ( " _ " , " - " )
2018-10-30 20:38:57 -05:00
@cache [ " #{ ip } - #{ locale } - #{ resolve_hostname } " ] || = lookup (
ip ,
locale : locale ,
resolve_hostname : resolve_hostname ,
)
2018-10-25 05:54:01 -05:00
end
def self . open_db ( path )
instance . open_db ( path )
2018-10-09 09:21:41 -05:00
end
2018-10-30 20:38:57 -05:00
def self . get ( ip , locale : :en , resolve_hostname : false )
instance . get ( ip , locale : locale , resolve_hostname : resolve_hostname )
2018-10-09 09:21:41 -05:00
end
end