{-# LANGUAGE CPP #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE RankNTypes #-} module Data.GeoIP2 ( -- * Library description -- | -- A haskell library for reading MaxMind's GeoIP version 2 files. -- It supports both IPv4 and IPv6 addresses. When a match is found, it -- is parsed and a simplified structure is returned. If you want to access -- other fields than those that are exposed, it is internally possible. -- -- The database is mmapped upon opening, all querying can be later -- performed purely without IO monad. -- * Opening the database GeoDB , openGeoDB, openGeoDBBS , geoDbLanguages, geoDbType, geoDbDescription , geoDbAddrType, GeoIP(..) , DecodeException(..) -- * Querying the database , findGeoData , GeoResult(..) , Location(..) , AS(..) -- * Internals , GeoField, GeoFieldT(..) , rawGeoData -- * Lenses , _DataString, _DataDouble, _DataInt, _DataWord , _DataMap, _DataArray, _DataBool, _DataUnknown , key , geoNum ) where #if !MIN_VERSION_base(4,8,0) import Control.Applicative ((<$>), (<*>)) #endif import Control.Monad (unless, when) import qualified Data.ByteString as BS import Data.Int import Data.IP (IP (..), ipv4ToIPv6) import Data.Maybe (mapMaybe) import Data.Serialize import qualified Data.Text as T import System.IO.MMap import Control.Lens (ix, (^?), _Just, to, (^..), Traversal', Fold, prism') import Control.Exception (throwIO, Exception) import Data.GeoIP2.Fields import Data.GeoIP2.SearchTree -- | Address type stored in database data GeoIP = GeoIPv6 | GeoIPv4 deriving (Eq, Show) data DecodeException = DecodeException String deriving (Show) instance Exception DecodeException -- | Handle for search operations data GeoDB = GeoDB { geoMem :: BS.ByteString , geoDbType :: T.Text -- ^ String that indicates the structure of each data record associated with an IP address , geoDbLanguages :: [T.Text] -- ^ Languages supported in database , geoDbNodeCount :: Int64 , geoDbRecordSize :: Int , geoDbAddrType :: GeoIP -- ^ Type of address (IPv4/IPv6) stored in a database , geoDbDescription :: Maybe T.Text -- ^ Description of a database in english } getHeaderBytes :: BS.ByteString -> BS.ByteString getHeaderBytes = lastsubstring "\xab\xcd\xefMaxMind.com" where lastsubstring pattern string = case BS.breakSubstring pattern string of (res, "") -> res (_, rest) -> lastsubstring pattern (BS.drop (BS.length pattern) rest) -- | Open database, mmap it into memory, parse header and return a handle for search operations -- This function may throw DecodeException openGeoDB :: FilePath -> IO GeoDB openGeoDB geoFile = do bsmem <- mmapFileByteString geoFile Nothing either (throwIO . DecodeException) return (openGeoDBBS bsmem) -- | Open database from a bytestring openGeoDBBS :: BS.ByteString -> Either String GeoDB openGeoDBBS bsmem = do hdr <- decode (getHeaderBytes bsmem) when (hdr ^? key "binary_format_major_version" . geoNum /= (Just 2 :: Maybe Int)) $ Left "Unsupported database version, only v2 supported." unless (hdr ^? key "record_size" . geoNum `elem` (Just <$> [24, 28, 32 :: Int])) $ Left "Record size not supported." let res = GeoDB bsmem <$> hdr ^? key "database_type" . _DataString <*> pure (hdr ^.. key "languages" . _DataArray . traverse . _DataString) <*> hdr ^? key "node_count" . geoNum <*> hdr ^? key "record_size" . geoNum <*> hdr ^? key "ip_version" . toVersion <*> pure (hdr ^? key "description" . key "en" . _DataString) maybe (Left "Error decoding header") return res where toVersion = geoNum . prism' pfrom pto where pfrom :: GeoIP -> Int pfrom GeoIPv4 = 4 pfrom GeoIPv6 = 6 pto 4 = Just GeoIPv4 pto 6 = Just GeoIPv6 pto _ = Nothing -- | Search GeoIP database and return complete unparsed data rawGeoData :: GeoDB -> IP -> Either String GeoField rawGeoData geodb addr = do bits <- coerceAddr offset <- getDataOffset (geoMem geodb, geoDbNodeCount geodb, geoDbRecordSize geodb) bits strictDataAt offset where dataSectionStart = (geoDbRecordSize geodb `div` 4) * fromIntegral (geoDbNodeCount geodb) + 16 dataSection = BS.drop dataSectionStart (geoMem geodb) strictDataAt :: Int64 -> Either String GeoField strictDataAt offset = do raw <- decode (BS.drop (fromIntegral offset) dataSection) traversePtr (strictDataAt . fromIntegral) raw coerceAddr | (IPv4 _) <- addr, GeoIPv4 <- geoDbAddrType geodb = return $ ipToBits addr | (IPv6 _) <- addr, GeoIPv6 <- geoDbAddrType geodb = return $ ipToBits addr | (IPv4 addrv4) <- addr, GeoIPv6 <- geoDbAddrType geodb = return $ ipToBits $ IPv6 (ipv4ToIPv6 addrv4) | otherwise = Left "Cannot search IPv6 address in IPv4 database" data AS = AS { asNumber :: Int , asOrganization :: T.Text } deriving (Show, Eq) -- | Result of a search query data GeoResult = GeoResult { geoContinent :: Maybe T.Text , geoContinentCode :: Maybe T.Text , geoCountryISO :: Maybe T.Text , geoCountry :: Maybe T.Text , geoLocation :: Maybe Location , geoCity :: Maybe T.Text , geoCityConfidence :: Maybe Int , geoPostalCode :: Maybe T.Text , geoAS :: Maybe AS , geoISP :: Maybe T.Text , geoOrganization :: Maybe T.Text , geoUserType :: Maybe T.Text , geoSubdivisions :: [(T.Text, T.Text)] } deriving (Show, Eq) -- | Location of the IP address data Location = Location { locationLatitude :: Double , locationLongitude :: Double , locationTimezone :: T.Text , locationAccuracy :: Maybe Int } deriving (Show, Eq) -- | Search GeoIP database findGeoData :: GeoDB -- ^ Db handle -> T.Text -- ^ Language code (e.g. "en") -> IP -- ^ IP address to search -> Either String GeoResult -- ^ Result, if something is found findGeoData geodb lang ip = do res <- rawGeoData geodb ip let subdivmap = res ^.. key "subdivisions" . _DataArray . traverse subdivs = mapMaybe (\s -> (,) <$> s ^? key "iso_code" . _DataString <*> s ^? key "names" . key lang . _DataString) subdivmap return $ GeoResult { geoContinent = res ^? key "continent" . key "names" . key lang . _DataString , geoContinentCode = res ^? key "continent" . key "code" . _DataString , geoCountryISO = res ^? key "country" . key "iso_code" . _DataString , geoCountry = res ^? key "country" . key "names" . key lang . _DataString , geoLocation = Location <$> res ^? key "location" . key "latitude" . _DataDouble <*> res ^? key "location" . key "longitude" . _DataDouble <*> res ^? key "location" . key "time_zone" . _DataString <*> pure (res ^? key "location" . key "accuracy_radius" . geoNum) , geoCity = res ^? key "city" . key "names" . key lang . _DataString , geoCityConfidence = res ^? key "city" . key "confidence" . geoNum , geoPostalCode = res ^? key "postal" . key "code" . _DataString , geoAS = AS <$> res ^? key "traits" . key "autonomous_system_number" . geoNum <*> res ^? key "traits" . key "autonomous_system_organization" . _DataString , geoISP = res ^? key "traits" . key "isp" . _DataString , geoOrganization = res ^? key "traits" . key "organization" . _DataString , geoUserType = res ^? key "traits" . key "user_type" . _DataString , geoSubdivisions = subdivs } -- | Helper lens to access key in a DataMap key :: T.Text -> Traversal' GeoField GeoField key k = _DataMap . ix (DataString k) -- | Helper lens to convert integer Word/Int to whatever number type is needed geoNum :: Num b => Fold GeoField b geoNum = to fromNum . _Just where fromNum (DataInt x) = Just (fromIntegral x) fromNum (DataWord x) = Just (fromIntegral x) fromNum _ = Nothing