GeoIp (empty) → 0.1
raw patch · 6 files changed
+259/−0 lines, 6 filesdep +basedep +bytestringdep +bytestring-mmapsetup-changed
Dependencies added: base, bytestring, bytestring-mmap
Files
- Copying +19/−0
- GeoIp.cabal +25/−0
- Network/GeoIp/GeoCityIp.hs +73/−0
- Network/GeoIp/GeoDB.hs +122/−0
- Setup.lhs +3/−0
- examples/TestGeoIp.hs +17/−0
+ Copying view
@@ -0,0 +1,19 @@+Copyright (c) 2008 Stephen Cook++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ GeoIp.cabal view
@@ -0,0 +1,25 @@+Name: GeoIp+Version: 0.1+License: OtherLicense+License-File: Copying+Author: Stephen Cook+Maintainer: siti@orcon.net.nz+category: Network+Synopsis: Pure bindings for the MaxMind IP database.+Stability: alpha+build-type: Simple+Description:+ GeoIp is a pure haskell binding to the MaxMind IP database.+ The IP database contains information such as the approximate+ longitude and latitude of an IP address.+Cabal-Version: >= 1.2++Library+ Exposed-modules:+ Network.GeoIp.GeoCityIp+ Other-modules:+ Network.GeoIp.GeoDB+ ghc-options: -Wall -fglasgow-exts+ Build-Depends: base, bytestring, bytestring-mmap++
+ Network/GeoIp/GeoCityIp.hs view
@@ -0,0 +1,73 @@++-- | This module loads the MaxMind's GeoIp City database.+module Network.GeoIp.GeoCityIp (+ GeoCityDB,+ load,+ findRange,+ findLocation) where++import qualified Data.ByteString as B+import Data.Int+import Data.Bits++import Network.GeoIp.GeoDB++data GeoCityDB = GeoCityDB GeoDB++-- | Load the city database. If an error is triggered then+-- Left is returned with an error string.+load :: FilePath -> IO (Either String GeoCityDB)+load geoFile = do+ eGeo <- makeGeoDB geoFile+ case eGeo of+ Left e -> return $ Left e+ Right geo ->+ if dataBaseType geo == geoIPCountryEdition then+ return $ Right (GeoCityDB geo)+ else+ return $ Left ("load: Incorrect database type. Database type is: " ++ (show $ dataBaseType geo))++extractRecordCity :: GeoDB -> Int -> (Double, Double)+extractRecordCity geo cursor =+ (latitude, longitude)+ where+ recordCursor = cursor + (2 * (recordLength geo) - 1) * (dataBaseSegments geo)+ memo = B.drop recordCursor (mem geo)+ (_, countryMem) = getBytes 1 memo+ (_, regionMem) = extractNullString countryMem+ (_, cityMem) = extractNullString regionMem+ (_, postalMem) = extractNullString cityMem+ (latitude, latMem) = ((fromIntegral ((getNumber $ B.take 3 postalMem)::Integer)) / 10000 - 180, B.drop 3 postalMem)+ (longitude, _) = ((fromIntegral ((getNumber $ B.take 3 latMem)::Integer)) / 10000 - 180, B.drop 3 latMem)+++generateMask :: forall a. (Num a, Bits a) => Int -> Int -> a+generateMask from to =+ if from <= to then+ (bit from) .|. generateMask (from+1) to+ else+ (bit from)++-- | Find the IP range that the IP address is in. The result is monadic.+-- In most cases you will want to use the Maybe monad.+findRange :: (Monad m) => GeoCityDB -> Integer -> m (Integer, Integer)+findRange (GeoCityDB geo) address = do+ (cursor, netMask) <- seekRecord geo address+ let+ bitMask = generateMask (31 - netMask) 31+ hostMask = generateMask 0 netMask+ if cursor == dataBaseSegments geo then+ fail "Could not find IP"+ else+ return (address .&. bitMask, address .|. hostMask) + +-- | Find the location of an IP address. The tuple returned is @(latitude, longitude)@.+-- The result is monadic, in most cases you will want to use the Maybe monad.+findLocation :: (Monad m) => GeoCityDB -> Integer -> m (Double, Double)+findLocation (GeoCityDB geo) address = do+ (cursor, _) <- seekRecord geo address+ if cursor == dataBaseSegments geo then+ fail "Could not find IP"+ else+ return (extractRecordCity geo cursor)+
+ Network/GeoIp/GeoDB.hs view
@@ -0,0 +1,122 @@+module Network.GeoIp.GeoDB where++import System.IO.Posix.MMap+import qualified Data.ByteString as B+import Data.Bits+import Data.Generics++data GeoDB = GeoDB {+ mem :: B.ByteString,+ dataBaseType :: Int,+ recordLength :: Int,+ dataBaseSegments :: Int+} deriving (Data, Typeable, Show)++makeGeoDB :: FilePath -> IO (Either String GeoDB)+makeGeoDB geoFile = do+ memo <- unsafeMMapFile geoFile+ let+ defaultGeo = GeoDB {mem = memo, dataBaseType = geoIPCountryEdition, recordLength = standardRecordLength, dataBaseSegments = countryBegin}+ eGeo = setupSegments defaultGeo ((B.length memo) - 3) 0+ return eGeo++setupSegments :: GeoDB -> Int -> Int -> Either String GeoDB+setupSegments geo cursor sizeRead+ | sizeRead >= structureMaxSize = Left "SetupSegments: could not find magic bytes. Incorrect file?"+ | otherwise = + if B.index (mem geo) cursor == 255 && B.index (mem geo) (cursor+1) == 255 && B.index (mem geo) (cursor+2) == 255 then+ let+ byteDbType = B.index (mem geo) (cursor+3)+ databaseType = if byteDbType >= 106 then (byteDbType - 105) else byteDbType+ in+ if databaseType == geoIPRegionEditionRev0 then+ Right geo { dataBaseSegments = stateBeginRev0 }+ else+ if databaseType == geoIPRegionEditionRev1 then+ Right geo { dataBaseSegments = stateBeginRev1 }+ else+ if (databaseType == geoIPCityEditionRev0 || databaseType == geoIPCityEditionRev1 || databaseType == geoIPOrgEdition || databaseType == geoIPIspEdition || databaseType == geoIPAsNumEdition) then+ let newgeo = geo {dataBaseSegments = getNumber $ B.take segmentRecordLength $ B.drop (cursor+4) (mem geo)}+ in+ if (databaseType == geoIPOrgEdition || databaseType == geoIPIspEdition) then+ Right newgeo {recordLength = orgRecordLength}+ else+ Right newgeo + else+ Right geo+ else+ setupSegments geo (cursor - 4) (sizeRead + 4)++getNumber :: forall b. (Bits b) => B.ByteString -> b+getNumber bytes = fst $ B.foldl combine (0, 0) bytes+ where+ combine (num, pos) b = (num + ((fromIntegral b) `shiftL` (pos*8)), pos + 1)++extractNullString :: B.ByteString -> (B.ByteString, B.ByteString)+extractNullString memo =+ (memTaken, memLeft)+ where+ memTaken = B.takeWhile (/=0) memo+ memLeft = B.drop ((B.length memTaken) + 1) memo++getBytes :: Int -> B.ByteString -> (B.ByteString, B.ByteString)+getBytes num memo = (B.take num memo, B.drop num memo)++seekRecord :: forall a m. (Bits a, Monad m) => GeoDB -> a -> m (Int, Int)+seekRecord geo ipNum = seekRecord_ geo 31 0 ipNum++seekRecord_ :: forall a m. (Bits a, Monad m) => GeoDB -> Int -> Int -> a -> m (Int, Int)+seekRecord_ geo depth offset ipNum+ | depth < 0 = fail "Could not find record. Database is possibly corrupt"+ | otherwise =+ if newOffset >= (dataBaseSegments geo) then+ return (newOffset, 32 - depth)+ else+ seekRecord_ geo (depth - 1) newOffset ipNum+ where+ cursor = (recordLength geo) * 2 * offset+ newOffset =+ if (ipNum .&. (1 `shiftL` depth) /= 0) then+ offsetFunction 1+ else+ offsetFunction 0+ offsetFunction pos = getNumber (B.take (recordLength geo) $ B.drop (cursor + (pos * (recordLength geo))) (mem geo))++geoIPCountryEdition :: forall n. (Num n) => n+geoIPCountryEdition = 1+geoIPRegionEditionRev0 :: forall n. (Num n) => n+geoIPRegionEditionRev0 = 7+geoIPCityEditionRev0 :: forall n. (Num n) => n+geoIPCityEditionRev0 = 6+geoIPOrgEdition :: forall n. (Num n) => n+geoIPOrgEdition = 5+geoIPIspEdition :: forall n. (Num n) => n+geoIPIspEdition = 4+geoIPCityEditionRev1 :: forall n. (Num n) => n+geoIPCityEditionRev1 = 2+geoIPRegionEditionRev1 :: forall n. (Num n) => n+geoIPRegionEditionRev1 = 3+--geoIPProxyEdition :: forall n. (Num n) => n+--geoIPProxyEdition = 8+geoIPAsNumEdition :: forall n. (Num n) => n+geoIPAsNumEdition = 9+--geoIPNetspeedEdition :: forall n. (Num n) => n+--geoIPNetspeedEdition = 10+--geoIPDomainEdition :: forall n. (Num n) => n+--geoIPDomainEdition = 11+structureMaxSize :: forall n. (Num n) => n+structureMaxSize = 20+segmentRecordLength :: forall n. (Num n) => n+segmentRecordLength = 3+standardRecordLength :: forall n. (Num n) => n+standardRecordLength = 3+orgRecordLength :: forall n. (Num n) => n+orgRecordLength = 4++stateBeginRev0 :: forall n. (Num n) => n+stateBeginRev0 = 16700000+stateBeginRev1 :: forall n. (Num n) => n+stateBeginRev1 = 16000000+countryBegin :: forall n. (Num n) => n+countryBegin = 16776960+
+ Setup.lhs view
@@ -0,0 +1,3 @@+#!/usr/bin/env runhaskell+> import Distribution.Simple+> main = defaultMainWithHooks defaultUserHooks
+ examples/TestGeoIp.hs view
@@ -0,0 +1,17 @@+import Network.GeoIp.GeoCityIp++main :: IO ()+main = do+ eGeo <- load "GeoLiteCity.dat"+ case eGeo of+ Left e -> do+ putStrLn e+ Right geo -> do+ let+ mRange :: Maybe (Integer, Integer)+ mRange = findRange geo 3413704712+ mLocation :: Maybe (Double, Double)+ mLocation = findLocation geo 3413704712+ putStrLn $ show mRange+ putStrLn $ show mLocation+ putStrLn "Data from c library in February 2008 is: 203.121.0.8 -> (3.166700, 101.699997)"