175 lines
4.8 KiB
Ruby
175 lines
4.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'maxmind/db'
|
|
require 'httparty'
|
|
require 'digest'
|
|
require 'tmpdir'
|
|
require 'fileutils'
|
|
|
|
class GeoIpService
|
|
include HTTParty
|
|
|
|
class DatabaseNotAvailable < StandardError; end
|
|
class InvalidIpAddress < StandardError; end
|
|
|
|
attr_reader :database_reader, :database_path
|
|
|
|
def initialize(database_path: nil)
|
|
@database_path = database_path || default_database_path
|
|
@database_reader = nil
|
|
load_database if File.exist?(@database_path)
|
|
end
|
|
|
|
# Main lookup method - returns country code for IP address
|
|
def lookup_country(ip_address)
|
|
return fallback_country unless database_available?
|
|
return fallback_country unless valid_ip?(ip_address)
|
|
|
|
result = database_reader.get(ip_address)
|
|
return fallback_country if result.nil? || result.empty?
|
|
|
|
# Extract country code from MaxMind result
|
|
result['country']&.[]('iso_code') || fallback_country
|
|
rescue => e
|
|
Rails.logger.error "GeoIP lookup failed for #{ip_address}: #{e.message}"
|
|
fallback_country
|
|
end
|
|
|
|
# Check if database is available and loaded
|
|
def database_available?
|
|
return false unless File.exist?(@database_path)
|
|
|
|
load_database unless database_reader
|
|
database_reader.present?
|
|
end
|
|
|
|
# Get database information
|
|
def database_info
|
|
return nil unless File.exist?(@database_path)
|
|
|
|
file_stat = File.stat(@database_path)
|
|
metadata = database_reader&.metadata
|
|
|
|
if metadata
|
|
{
|
|
type: 'GeoLite2-Country',
|
|
version: "#{metadata.binary_format_major_version}.#{metadata.binary_format_minor_version}",
|
|
size: file_stat.size,
|
|
modified_at: file_stat.mtime,
|
|
age_days: ((Time.current - file_stat.mtime) / 1.day).round,
|
|
file_path: @database_path
|
|
}
|
|
else
|
|
{
|
|
type: 'GeoLite2-Country',
|
|
version: 'Unknown',
|
|
size: file_stat.size,
|
|
modified_at: file_stat.mtime,
|
|
age_days: ((Time.current - file_stat.mtime) / 1.day).round,
|
|
file_path: @database_path
|
|
}
|
|
end
|
|
end
|
|
|
|
# Class method for convenience lookup
|
|
def self.lookup_country(ip_address)
|
|
new.lookup_country(ip_address)
|
|
end
|
|
|
|
# Update database from remote source
|
|
def self.update_database!
|
|
new.update_from_remote!
|
|
end
|
|
|
|
# Download and install database from remote URL
|
|
def update_from_remote!
|
|
config = Rails.application.config.maxmind
|
|
database_url = config.database_url
|
|
storage_path = config.storage_path
|
|
database_filename = config.database_filename
|
|
temp_file = nil
|
|
|
|
Rails.logger.info "Starting GeoIP database download from #{database_url}"
|
|
|
|
begin
|
|
# Ensure storage directory exists
|
|
FileUtils.mkdir_p(storage_path) unless Dir.exist?(storage_path)
|
|
|
|
# Download to temporary file
|
|
Dir.mktmpdir do |temp_dir|
|
|
temp_file = File.join(temp_dir, database_filename)
|
|
|
|
response = HTTParty.get(database_url, timeout: 60)
|
|
raise "Failed to download database: #{response.code}" unless response.success?
|
|
|
|
File.binwrite(temp_file, response.body)
|
|
|
|
# Validate downloaded file
|
|
validate_downloaded_file(temp_file)
|
|
|
|
# Move to final location
|
|
final_path = File.join(storage_path, database_filename)
|
|
File.rename(temp_file, final_path)
|
|
|
|
# Reload the database with new file
|
|
@database_reader = nil
|
|
load_database
|
|
|
|
Rails.logger.info "GeoIP database successfully updated: #{final_path}"
|
|
return true
|
|
end
|
|
rescue => e
|
|
Rails.logger.error "Failed to update GeoIP database: #{e.message}"
|
|
File.delete(temp_file) if temp_file && File.exist?(temp_file)
|
|
false
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def load_database
|
|
return unless File.exist?(@database_path)
|
|
|
|
@database_reader = MaxMind::DB.new(@database_path)
|
|
rescue => e
|
|
Rails.logger.error "Failed to load GeoIP database: #{e.message}"
|
|
@database_reader = nil
|
|
end
|
|
|
|
def default_database_path
|
|
config = Rails.application.config.maxmind
|
|
File.join(config.storage_path, config.database_filename)
|
|
end
|
|
|
|
def valid_ip?(ip_address)
|
|
IPAddr.new(ip_address)
|
|
true
|
|
rescue IPAddr::InvalidAddressError
|
|
false
|
|
end
|
|
|
|
def fallback_country
|
|
config = Rails.application.config.maxmind
|
|
return nil unless config.enable_fallback
|
|
config.fallback_country
|
|
end
|
|
|
|
def cache_size
|
|
return 0 unless Rails.application.config.maxmind.cache_enabled
|
|
Rails.application.config.maxmind.cache_size
|
|
end
|
|
|
|
def validate_downloaded_file(file_path)
|
|
# Basic file existence and size check
|
|
raise "Downloaded file is empty" unless File.exist?(file_path)
|
|
raise "Downloaded file is too small" if File.size(file_path) < 1_000_000 # ~1MB minimum
|
|
|
|
# Try to open with MaxMind reader
|
|
begin
|
|
MaxMind::DB.new(file_path)
|
|
rescue => e
|
|
raise "Invalid MaxMind database format: #{e.message}"
|
|
end
|
|
end
|
|
end
|