99 lines
2.5 KiB
Elixir
99 lines
2.5 KiB
Elixir
defmodule Towerops.GeoIP do
|
|
@moduledoc """
|
|
IP geolocation lookup using PostgreSQL database.
|
|
|
|
Provides fast lookups for country and city information from IP addresses
|
|
using the MaxMind GeoLite2-City database stored in PostgreSQL.
|
|
"""
|
|
import Ecto.Query
|
|
|
|
alias Towerops.GeoIP.Block
|
|
alias Towerops.GeoIP.Location
|
|
alias Towerops.Repo
|
|
|
|
@doc """
|
|
Looks up the country code for an IP address.
|
|
|
|
Returns the 2-letter country code or nil if not found.
|
|
|
|
## Examples
|
|
|
|
iex> Towerops.GeoIP.lookup("8.8.8.8")
|
|
"US"
|
|
|
|
iex> Towerops.GeoIP.lookup("invalid")
|
|
nil
|
|
"""
|
|
def lookup(ip_string) when is_binary(ip_string) do
|
|
case lookup_full(ip_string) do
|
|
%{country_code: country_code} -> country_code
|
|
nil -> nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Looks up full location information for an IP address.
|
|
|
|
Returns a map with country_code, country_name, city_name, and region information,
|
|
or nil if not found.
|
|
|
|
## Examples
|
|
|
|
iex> Towerops.GeoIP.lookup_full("8.8.8.8")
|
|
%{
|
|
country_code: "US",
|
|
country_name: "United States",
|
|
city_name: "Mountain View",
|
|
subdivision_1_name: "California",
|
|
subdivision_2_name: nil,
|
|
latitude: 37.386,
|
|
longitude: -122.0838
|
|
}
|
|
|
|
iex> Towerops.GeoIP.lookup_full("invalid")
|
|
nil
|
|
"""
|
|
def lookup_full(ip_string) when is_binary(ip_string) do
|
|
case parse_ip(ip_string) do
|
|
{:ok, ip_int} ->
|
|
find_location(ip_int)
|
|
|
|
:error ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
defp find_location(ip_int) do
|
|
# Query for IP block that contains this IP address
|
|
# Uses index on (start_ip_int, end_ip_int) for fast range lookup
|
|
query =
|
|
from b in Block,
|
|
join: l in Location,
|
|
on: b.geoname_id == l.geoname_id,
|
|
where: b.start_ip_int <= ^ip_int and b.end_ip_int >= ^ip_int,
|
|
select: %{
|
|
country_code: l.country_code,
|
|
country_name: l.country_name,
|
|
city_name: l.city_name,
|
|
subdivision_1_name: l.subdivision_1_name,
|
|
subdivision_2_name: l.subdivision_2_name,
|
|
latitude: l.latitude,
|
|
longitude: l.longitude
|
|
},
|
|
limit: 1
|
|
|
|
Repo.one(query)
|
|
end
|
|
|
|
defp parse_ip(ip_string) do
|
|
case :inet.parse_address(String.to_charlist(ip_string)) do
|
|
{:ok, {a, b, c, d}} ->
|
|
# Convert IP to integer for range comparison
|
|
ip_int = a * 256 * 256 * 256 + b * 256 * 256 + c * 256 + d
|
|
{:ok, ip_int}
|
|
|
|
_ ->
|
|
:error
|
|
end
|
|
end
|
|
end
|