Accepts incoming events and correctly parses them into events. GeoLite2 integration complete"

This commit is contained in:
Dan Milne
2025-11-04 00:11:10 +11:00
parent 0cbd462e7c
commit 5ff166613e
49 changed files with 4489 additions and 322 deletions

View File

@@ -0,0 +1,174 @@
# 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