159 lines
5.4 KiB
Ruby
159 lines
5.4 KiB
Ruby
class Ipapi
|
|
include HTTParty
|
|
BASE_URL = "https://api.ipapi.is/"
|
|
|
|
def api_key = Setting.ipapi_key
|
|
|
|
def lookup(ip)
|
|
return unless api_key.present?
|
|
response = self.class.get("#{BASE_URL}", query: { q: ip, key: api_key })
|
|
response.parsed_response
|
|
end
|
|
|
|
def self.lookup(ip) = new.lookup(ip)
|
|
|
|
def multi_lookup(ips)
|
|
ips = Array(ips)
|
|
ips.each_slice(100).flat_map { |slice| post_data({ips: slice}) }
|
|
end
|
|
|
|
def data(ip)
|
|
if ip.is_a?(Array)
|
|
post_data(ip)
|
|
else
|
|
response = self.class.get("#{BASE_URL}", query: { q: ip, key: api_key })
|
|
response.parsed_response
|
|
end
|
|
rescue JSON::ParserError
|
|
{}
|
|
end
|
|
|
|
def post_data(ips)
|
|
response = self.class.post("#{BASE_URL}",
|
|
query: { key: api_key },
|
|
body: { ips: ips }.to_json,
|
|
headers: { 'Content-Type' => 'application/json' }
|
|
)
|
|
|
|
results = response.parsed_response
|
|
|
|
results["response"].map do |ip, data|
|
|
IPAddr.new(ip)
|
|
cidr = data.dig("asn", "route")
|
|
|
|
network_range = NetworkRange.add_network(cidr)
|
|
if network_range
|
|
network_range.set_network_data(:ipapi, data)
|
|
network_range.last_api_fetch = Time.current
|
|
network_range.save
|
|
end
|
|
network_range
|
|
rescue IPAddr::InvalidAddressError
|
|
puts "Skipping #{ip}"
|
|
next
|
|
end
|
|
end
|
|
|
|
# Parse company/datacenter network range from IPAPI data
|
|
# Handles "X.X.X.X - Y.Y.Y.Y" format and converts to CIDR
|
|
def self.parse_company_network_range(ipapi_data)
|
|
# Try company.network first, then datacenter.network
|
|
network_range = ipapi_data.dig('company', 'network') || ipapi_data.dig('datacenter', 'network')
|
|
return nil if network_range.blank?
|
|
|
|
# Parse "X.X.X.X - Y.Y.Y.Y" format
|
|
if network_range.include?(' - ')
|
|
start_ip_str, end_ip_str = network_range.split(' - ').map(&:strip)
|
|
|
|
begin
|
|
start_ip = IPAddr.new(start_ip_str)
|
|
end_ip = IPAddr.new(end_ip_str)
|
|
|
|
# Calculate the number of IPs in the range
|
|
num_ips = end_ip.to_i - start_ip.to_i + 1
|
|
|
|
# Calculate prefix length from number of IPs
|
|
# num_ips = 2^(32 - prefix_length) for IPv4
|
|
prefix_length = 32 - Math.log2(num_ips).to_i
|
|
|
|
# Verify it's a valid CIDR block (power of 2)
|
|
if 2**(32 - prefix_length) == num_ips
|
|
cidr = "#{start_ip_str}/#{prefix_length}"
|
|
Rails.logger.debug "Parsed company network range: #{network_range} -> #{cidr}"
|
|
return cidr
|
|
else
|
|
Rails.logger.warn "Network range #{network_range} is not a valid CIDR block (#{num_ips} IPs)"
|
|
return nil
|
|
end
|
|
rescue IPAddr::InvalidAddressError => e
|
|
Rails.logger.error "Invalid IP in company network range: #{network_range} (#{e.message})"
|
|
return nil
|
|
end
|
|
elsif network_range.include?('/')
|
|
# Already in CIDR format
|
|
return network_range
|
|
else
|
|
Rails.logger.warn "Unknown network range format: #{network_range}"
|
|
return nil
|
|
end
|
|
end
|
|
|
|
# Populate NetworkRange attributes from IPAPI data
|
|
def self.populate_network_attributes(network_range, ipapi_data)
|
|
network_range.asn = ipapi_data.dig('asn', 'asn')
|
|
network_range.asn_org = ipapi_data.dig('asn', 'org') || ipapi_data.dig('company', 'name')
|
|
network_range.company = ipapi_data.dig('company', 'name')
|
|
network_range.country = ipapi_data.dig('location', 'country_code')
|
|
network_range.is_datacenter = ipapi_data['is_datacenter'] || false
|
|
network_range.is_vpn = ipapi_data['is_vpn'] || false
|
|
network_range.is_proxy = ipapi_data['is_proxy'] || false
|
|
end
|
|
|
|
# Process IPAPI data and create network ranges
|
|
# Returns array of created/updated NetworkRange objects
|
|
def self.process_ipapi_data(ipapi_data, tracking_network)
|
|
created_networks = []
|
|
|
|
# Extract and create company/datacenter network range if present
|
|
company_network_cidr = parse_company_network_range(ipapi_data)
|
|
if company_network_cidr.present?
|
|
company_range = NetworkRange.find_or_create_by(network: company_network_cidr) do |nr|
|
|
nr.source = 'api_imported'
|
|
nr.creation_reason = "Company allocation from IPAPI for #{tracking_network.cidr}"
|
|
end
|
|
|
|
# Always update attributes (whether new or existing)
|
|
populate_network_attributes(company_range, ipapi_data)
|
|
company_range.set_network_data(:ipapi, ipapi_data)
|
|
company_range.last_api_fetch = Time.current
|
|
company_range.save!
|
|
|
|
created_networks << company_range
|
|
Rails.logger.info "Created/updated company network: #{company_range.cidr}"
|
|
end
|
|
|
|
# Extract and create ASN route network if present
|
|
ipapi_route = ipapi_data.dig('asn', 'route')
|
|
if ipapi_route.present? && ipapi_route != tracking_network.cidr
|
|
route_network = NetworkRange.find_or_create_by(network: ipapi_route) do |nr|
|
|
nr.source = 'api_imported'
|
|
nr.creation_reason = "BGP route from IPAPI lookup for #{tracking_network.cidr}"
|
|
end
|
|
|
|
# Always update attributes (whether new or existing)
|
|
populate_network_attributes(route_network, ipapi_data)
|
|
route_network.set_network_data(:ipapi, ipapi_data)
|
|
route_network.last_api_fetch = Time.current
|
|
route_network.save!
|
|
|
|
created_networks << route_network
|
|
Rails.logger.info "Created/updated BGP route network: #{route_network.cidr}"
|
|
end
|
|
|
|
# Return both the created networks and the broadest CIDR for deduplication
|
|
{
|
|
networks: created_networks,
|
|
broadest_cidr: company_network_cidr.presence || ipapi_route || tracking_network.cidr
|
|
}
|
|
end
|
|
end |